Generating files with one button press in some entity view

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • Stanson
    Junior Member
    • Jun 2020
    • 4

    Generating files with one button press in some entity view

    I use this method for cross-compiling fresh version of Windows software package for specific device on the linux server and add this file to the current Stock entity .
    Building is a long process, so I use Job feature to start process from cron, as described at
    https://github.com/espocrm/documenta...opment/jobs.md

    You could use this solution to generate anything you want on your linux server running EspoCRM using any linux software.

    In example I use a custom entity named Stock with some fields and a relation to Documents.

    Of course this example could be easily modified for any other entity, existing or new, just replace Stock with your entity.

    Controller for Entity for Api calls. It creates a Job with current Entity data.
    File custom/Espo/Custom/Controllers/Stock.php
    Code:
    <?php
    
    namespace Espo\Custom\Controllers;
    
    class Stock extends \Espo\Core\Templates\Controllers\Base
    {
        public function getActionBuild($params, $data, $request)
        {
            $this->getEntityManager()->createEntity('Job', [
                'serviceName' => 'BuildService',
                'methodName' => 'buildProg',
                'data' => (object) [ // Get fields from current entity
                    'stock'  => $request->get('id'),
                    'device' => $request->get('device'),
                    'serial' => $request->get('serial'),
                ],
                'executeTime' => date('Y-m-d H:i:s'), // you can delay execution by setting a later time
                'queue' => 'q0', // available queues are listed below
            ]);
    
            return true; // can be true, false, array or object.
        }
    }
    This is a Job. Job will be run as crond child.
    File custom/Espo/Custom/Services/BuildService.php
    Code:
    <?php
    
    namespace Espo\Custom\Services;
    
    class BuildService extends \Espo\Core\Services\Base
    {
        protected function init()
        {
            $this->addDependencyList([
                'entityManager',
            ]);
        }
    
        public function buildProg($data)
        {
            $stockId = $data->stock;
            $device  = $data->device;
            $serial  = $data->serial;
    
            // Creating configuration file for build
            $fd = fopen( "/path_to_your_EspoCRM//build/config.mk", "w" );
            {
                fwrite( $fd, "HW_DEVICES = " . $device . "\n" );
                fwrite( $fd, $device . "_SERIAL = " . $serial . "\n" );
                fwrite( $fd, $device . "_BACKEND = libusb0\n" );
                fclose( $fd );
            }
    
            // Running build process. This is a shell script with build commands with output redirected to log file.
            // To return filename to PHP we just echo resulting executable name at last line of www-build script
            $fileName = exec( "/path_to_your_EspoCRM/build/www-build", $foo, $err );
    
            if( $err || !$fileName ) return false;
    
            $fileSize = filesize( $fileName );
    
            if( !$fileSize ) return false;
    
    //      // Parse program output if resulting filename returned in build script exhaust
    //      $param = array();
    //      foreach( $foo => $s )
    //      {
    //          $a = explode( '=', $s );
    //          $param[ trim( $a[0] ) ] = trim( $a[1] );
    //      }
    
            $em = $this->getInjection('entityManager');
    
            // Create new document
            $document = $em->createEntity( 'Document', [
                'name' => basename( $fileName ),
            ]);
    
            // Create empty attachment with real file size
            $attachment = $em->createEntity( 'Attachment', [
                'name' => basename( $fileName ),
                'size' => $fileSize,
                'type' => 'application/x-ms-dos-executable',
                'role' => 'Attachment',
                'contents' => '',
                'relatedId' => $document->id,
                'relatedType' => 'Document',
            ]);
    
            // Small hack - replace empty attachment file with our fresh executable
            $tmp = realpath( $em->getRepository( 'Attachment' )->getFilePath( $attachment ) );
            unlink( $tmp );
            rename( $fileName, $tmp );
    
            // Relate to Document
            $em->getRepository( 'Document' )->relate( $document, 'file', $attachment );
    
            // Relate to Folder
            $fr = $em->getRepository( 'DocumentFolder' );
            $folder = $fr->where([ 'name' => 'GetSpectrum' ])->findOne();
            $fr->relate( $folder, 'documents', $document );
    
            // Relate to Stock
            $sr = $em->getRepository( 'Stock' );
            $stock = $sr->get( $stockId );
            $sr->relate( $stock, 'documents', $document );
    
            return true;
        }
    }
    Adding a button to Entity detail view
    File custom/Espo/Custom/Resources/metadata/clientDefs/Stock.json
    Code:
    {
        "controller": "controllers/record",
        "boolFilterList": [
            "onlyMy"
        ],
        "kanbanViewMode": false,
        "color": "#c7ed55",
        "iconClass": "fas fa-warehouse",
        "menu": {
            "detail": {
                "buttons": [
                    "__APPEND__",
                    {
                        "action": "buildProg",
                        "label": "Build Program",
                        "style": "default",
                        "acl": "edit",
                        "aclScope": "Stock",
                        "data": {
                            "handler": "custom:build-action-handler"
                        }
                    }
                ]
            }
        }
    }
    This will run on frontend when user press a button
    File client/custom/src/build-action-handler.js
    Code:
    define('custom:build-action-handler', ['action-handler'], function (Dep) {
    
       return Dep.extend({
    
            actionBuildProg: function (data, e) {
                var that = this;
                Espo.Ajax.getRequest('Stock/' + this.view.model.id).then(function (response) {
                    var req = 'Stock/action/build'
                            + '?id=' + response.id
                            + '&device=' + response.name
                            + '&serial=' + response.serialNumber;
                    Espo.Ajax.getRequest(req).then(function (res) {
                        // show notification that build in process
                        Espo.Ui.notify('Building program...  ', 'dismissable', 0, true);
                        setTimeout(that.timerBuildCheck, 30000, that, res.jobId);
                    });
                });
            },
    
            // check job status periodically to show notification and update entity page to make new file visible
            timerBuildCheck: function (that, jobId) {
                Espo.Ajax.getRequest('Job/' + jobId).then(function (response) {
                    if(response.status == 'Success') {
                        Espo.Ui.notify('Program built', 'success', 3000);
                        that.view.model.trigger('update-all');
                        return;
                    }
                    if(response.status == 'Failed') {
                        Espo.Ui.notify('Build error', 'warning', 3000);
                        return;
                    }
                    setTimeout(that.timerBuildCheck, 5000, that, jobId);
                });
            },
    
            controlButtonVisibility: function () {
                if (~['Converted', 'Dead', 'Recycled'].indexOf(this.view.model.get('status'))) {
                    this.view.hideHeaderActionItem('buildGS');
                } else {
                    this.view.showHeaderActionItem('buildGS');
                }
            }
       });
    });
    You are free to use this code as you wish.
    Last edited by Stanson; 06-29-2020, 06:07 AM.
  • esforim
    Active Community Member
    • Jan 2020
    • 2204

    #2
    Thank you. Eventually will look to trying this out.

    From my understanding, it Export the Entity "Stock" to a file so you can import it into a different server (or use as a backup)?

    Comment


    • Stanson
      Stanson commented
      Editing a comment
      It creates a Job with Entity data, then, cron starts this job as a separate process and that Job creates confg file, start external program, gets program results (file and exit code) and creates Entity related Document.
      The Job is necessary to avoid running process taking long time as a child of web-server. Also, this gives an ability to get process results into EspoCRM and create a related Document on success.

      If your external process is not time-consuming, or you just want to get a file with Entity data you could do it directly in Entity Controller, without starting a Job.

      Even if process is long and until you dont need process result in EspoCRM you could just use exec("setsid your-program arg0 arg1... >/dev/null 2>&1 &"); in Entity Controller to detach process from web-server and leave it running independently.
  • Stanson
    Junior Member
    • Jun 2020
    • 4

    #3
    Even simplier variant of above with less code. Only Entity id is transferred to Job, so, less data exchange between frontend, Controller and Job and less code

    Really, we, maybe, could create a Job right in Button JS, but using Entity Controller gives more control.

    Controller: custom/Espo/Custom/Controllers/Stock.php
    Code:
    <?php
    
    namespace Espo\Custom\Controllers;
    
    class Stock extends \Espo\Core\Templates\Controllers\Base
    {
        public function getActionBuild($params, $data, $request)
        {
            $job = $this->getEntityManager()->createEntity('Job', [
                'serviceName' => 'BuildService',
                'methodName'  => 'buildProg',
                'executeTime' => date('Y-m-d H:i:s'), // you can delay execution by setting a later time
                'queue'       => 'q0', // available queues are listed below
                'data'        => (object) [ 'stock' => $request->get('id') ],
            ]);
    
            return (object) [ 'jobId' => $job->id ]; // can be true, false, array or object.
        }
    }
    Job: custom/Espo/Custom/Services/BuildService.php
    Code:
    <?php
    
    namespace Espo\Custom\Services;
    
    class BuildService extends \Espo\Core\Services\Base
    {
        protected function init()
        {
            $this->addDependencyList([
                'entityManager',
            ]);
        }
    
        public function buildProg( $data )
        {
            $em = $this->getInjection('entityManager');
            $sr = $em->getRepository( 'Stock' );
            $stock = $sr->get( $data->stock );
            $data = $stock->getValueMap();
    
            $desc = 'For ' . $data->name . ' #' . $data->serialNumber;
    
            $fd = fopen( "your_path_to_EspoCRM/build/config.mk", "w" );
            if( $fd )
            {
                fwrite( $fd, "HW_DEVICES = " . $data->name . "\n" );
                fwrite( $fd, $data->name . "_SERIAL = " . $data->serialNumber . "\n" );
                fwrite( $fd, $data->name . "_BACKEND = libusb0\n" );
                fclose( $fd );
            }
    
            // Running build process. This is a shell script with build commands with output redirected to log file.
            // To return filename to PHP we just echo resulting executable name at last line of www-build script
            $fileName = exec( "/your_path_to_EspoCRM/build/www-build", $foo, $err );
    
            if( $err || !$fileName ) return false;
    
            $fileSize = filesize( $fileName );
    
            if( !$fileSize ) return false;
    
    //      // Parse program output if resulting filename returned in build script exhaust
    //      $param = array();
    //      foreach( $foo => $s )
    //      {
    //          $a = explode( '=', $s );
    //          $param[ trim( $a[0] ) ] = trim( $a[1] );
    //      }
    
           // Create new document
           $document = $em->createEntity( 'Document', [
                'name'        => basename( $fileName ),
                'status'      => 'Active',
                'description' => $desc
            ]);
    
            // Create empty attachment with real file size
            $attachment = $em->createEntity( 'Attachment', [
                'name' => basename( $fileName ),
                'size' => $fileSize,
                'type' => 'application/x-ms-dos-executable',
                'role' => 'Attachment',
                'contents' => '',
                'relatedId' => $document->id,
                'relatedType' => 'Document',
            ]);
    
            // Small hack - replace empty attachment file with our fresh executable
            $tmp = realpath( $em->getRepository( 'Attachment' )->getFilePath( $attachment ) );
            unlink( $tmp );
            rename( $fileName, $tmp );
    
            // Relate to Document
            $em->getRepository( 'Document' )->relate( $document, 'file', $attachment );
    
            // Relate to Folder
            $fr = $em->getRepository( 'DocumentFolder' );
            $folder = $fr->where([ 'name' => 'GetSpectrum' ])->findOne();
            $fr->relate( $folder, 'documents', $document );
    
            // Relate to Stock
            $sr->relate( $stock, 'documents', $document );
    
            return true;
        }
    }
    Button: custom/Espo/Custom/Resources/metadata/clientDefs/Stock.json
    Code:
    {
        "controller": "controllers/record",
        "boolFilterList": [
            "onlyMy"
        ],
        "kanbanViewMode": false,
        "color": "#c7ed55",
        "iconClass": "fas fa-warehouse",
        "menu": {
            "detail": {
                "buttons": [
                    "__APPEND__",
                    {
                        "action": "buildProg",
                        "label": "Build Program",
                        "style": "default",
                        "acl": "edit",
                        "aclScope": "Stock",
                        "data": {
                            "handler": "custom:build-action-handler"
                        }
                    }
                ]
            }
        }
    }
    Button script: client/custom/src/build-action-handler.js
    Code:
    define('custom:build-action-handler', ['action-handler'], function (Dep) {
    
       return Dep.extend({
    
            actionBuildProg: function (data, e) {
                var that = this;
                var req = 'Stock/action/build?id=' + this.view.model.id;
                Espo.Ajax.getRequest(req).then(function (res) {
                    Espo.Ui.notify('Building program...  ', 'dismissable', 0, true);
                    setTimeout(that.timerBuildCheck, 30000, that, res.jobId);
                });
            },
    
            timerBuildCheck: function (that, jobId) {
                Espo.Ajax.getRequest('Job/' + jobId).then(function (response) {
                    if(response.status == 'Success') {
                        Espo.Ui.notify('Program built', 'success', 3000);
                        that.view.model.trigger('update-all');
                        return;
                    }
                    if(response.status == 'Failed') {
                        Espo.Ui.notify('Build error', 'warning', 3000);
                        return;
                    }
                    setTimeout(that.timerBuildCheck, 5000, that, jobId);
                });
            },
    
            controlButtonVisibility: function () {
                if (~['Converted', 'Dead', 'Recycled'].indexOf(this.view.model.get('status'))) {
                    this.view.hideHeaderActionItem('buildGS');
                } else {
                    this.view.showHeaderActionItem('buildGS');
                }
            }
       });
    });
    Last edited by Stanson; 06-29-2020, 07:57 AM.

    Comment

    Working...