Source of file WorkflowDefinition.php
Size: 15,032 Bytes - Last Modified: 2021-12-23T10:11:37+00:00
/var/www/docs.ssmods.com/process/src/code/dataobjects/WorkflowDefinition.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 | <?php use SilverStripe\ORM\DB; use SilverStripe\ORM\DataObject; use SilverStripe\Security\Member; use SilverStripe\Security\Permission; /** * An overall definition of a workflow * * The workflow definition has a series of steps to it. Each step has a series of possible transitions * that it can take - the first one that meets certain criteria is followed, which could lead to * another step. * * A step is either manual or automatic; an example 'manual' step would be requiring a person to review * a document. An automatic step might be to email a group of people, or to publish documents. * Basically, a manual step requires the interaction of someone to pick which action to take, an automatic * step will automatically determine what to do once it has finished. * * @author marcus@silverstripe.com.au * @license BSD License (http://silverstripe.org/bsd-license/) * @package advancedworkflow */ class WorkflowDefinition extends DataObject { private static $db = array( 'Title' => 'Varchar(128)', 'Description' => 'Text', 'Template' => 'Varchar', 'TemplateVersion' => 'Varchar', 'RemindDays' => 'Int', 'Sort' => 'Int', 'InitialActionButtonText' => 'Varchar', ); private static $default_sort = 'Sort'; private static $has_many = array( 'Actions' => 'WorkflowAction', 'Instances' => 'WorkflowInstance' ); /** * By default, a workflow definition is bound to a particular set of users or groups. * * This is covered across to the workflow instance - it is up to subsequent * workflow actions to change this if needbe. * * @var array */ private static $many_many = array( 'Users' => 'SilverStripe\\Security\\Member', 'Groups' => 'SilverStripe\\Security\\Group' ); private static $icon = 'advancedworkflow/images/definition.png'; public static $default_workflow_title_base = 'My Workflow'; public static $workflow_defs = array(); private static $dependencies = array( 'workflowService' => '%$WorkflowService', ); /** * @var WorkflowService */ public $workflowService; /** * Gets the action that first triggers off the workflow * * @return WorkflowAction */ public function getInitialAction() { if($actions = $this->Actions()) return $actions->First(); } /** * Ensure a sort value is set and we get a useable initial workflow title. */ public function onBeforeWrite() { if(!$this->Sort) { $this->Sort = DB::query('SELECT MAX("Sort") + 1 FROM "WorkflowDefinition"')->value(); } if(!$this->ID && !$this->Title) { $this->Title = $this->getDefaultWorkflowTitle(); } parent::onBeforeWrite(); } /** * After we've been written, check whether we've got a template and to then * create the relevant actions etc. */ public function onAfterWrite() { parent::onAfterWrite(); // Request via ImportForm where TemplateVersion is already set, so unset it $posted = Controller::curr()->getRequest()->postVars(); if(isset($posted['_CsvFile']) && $this->TemplateVersion) { $this->TemplateVersion = null; } if($this->numChildren() == 0 && $this->Template && !$this->TemplateVersion) { $this->workflowService->defineFromTemplate($this, $this->Template); } } /** * Ensure all WorkflowDefinition relations are removed on delete. If we don't do this, * we see issues with targets previously under the control of a now-deleted workflow, * becoming stuck, even if a new workflow is subsequently assigned to it. * * @return null */ public function onBeforeDelete() { parent::onBeforeDelete(); // Delete related import $this->deleteRelatedImport(); // Reset/unlink related HasMany|ManyMany relations and their orphaned objects $this->removeRelatedHasLists(); } /** * Removes User+Group relations from this object as well as WorkflowAction relations. * When a WorkflowAction is deleted, its own relations are also removed: * - WorkflowInstance * - WorkflowTransition * @see WorkflowAction::onAfterDelete() * * @return void */ private function removeRelatedHasLists() { $this->Users()->removeAll(); $this->Groups()->removeAll(); $this->Actions()->each(function($action) { if($orphan = DataObject::get_by_id('WorkflowAction', $action->ID)) { $orphan->delete(); } }); } /** * * Deletes related ImportedWorkflowTemplate objects. * * @return void */ private function deleteRelatedImport() { if($import = DataObject::get('ImportedWorkflowTemplate')->filter('DefinitionID', $this->ID)->first()) { $import->delete(); } } /** * @return int */ public function numChildren() { return count($this->Actions()); } public function fieldLabels($includerelations = true) { $labels = parent::fieldLabels($includerelations); $labels['Title'] = _t('WorkflowDefinition.TITLE', 'Title'); $labels['Description'] = _t('WorkflowDefinition.DESCRIPTION', 'Description'); $labels['Template'] = _t('WorkflowDefinition.TEMPLATE_NAME', 'Source Template'); $labels['TemplateVersion'] = _t('WorkflowDefinition.TEMPLATE_VERSION', 'Template Version'); return $labels; } public function getCMSFields() { $cmsUsers = Member::mapInCMSGroups(); $fields = new FieldList(new TabSet('Root')); $fields->addFieldToTab('Root.Main', new TextField('Title', $this->fieldLabel('Title'))); $fields->addFieldToTab('Root.Main', new TextareaField('Description', $this->fieldLabel('Description'))); $fields->addFieldToTab('Root.Main', TextField::create( 'InitialActionButtonText', _t('WorkflowDefinition.INITIAL_ACTION_BUTTON_TEXT', 'Initial Action Button Text') )); if($this->ID) { $fields->addFieldToTab('Root.Main', new CheckboxSetField('Users', _t('WorkflowDefinition.USERS', 'Users'), $cmsUsers)); $fields->addFieldToTab('Root.Main', new TreeMultiselectField('Groups', _t('WorkflowDefinition.GROUPS', 'Groups'), 'SilverStripe\\Security\\Group')); } if (class_exists('AbstractQueuedJob')) { $before = _t('WorkflowDefinition.SENDREMINDERDAYSBEFORE', 'Send reminder email after '); $after = _t('WorkflowDefinition.SENDREMINDERDAYSAFTER', ' days without action.'); $fields->addFieldToTab('Root.Main', new FieldGroup( _t('WorkflowDefinition.REMINDEREMAIL', 'Reminder Email'), new LabelField('ReminderEmailBefore', $before), new NumericField('RemindDays', ''), new LabelField('ReminderEmailAfter', $after) )); } if($this->ID) { if ($this->Template) { $template = $this->workflowService->getNamedTemplate($this->Template); $fields->addFieldToTab('Root.Main', new ReadonlyField('Template', $this->fieldLabel('Template'), $this->Template)); $fields->addFieldToTab('Root.Main', new ReadonlyField('TemplateDesc', _t('WorkflowDefinition.TEMPLATE_INFO', 'Template Info'), $template ? $template->getDescription() : '')); $fields->addFieldToTab('Root.Main', $tv = new ReadonlyField('TemplateVersion', $this->fieldLabel('TemplateVersion'))); $tv->setRightTitle(sprintf(_t('WorkflowDefinition.LATEST_VERSION', 'Latest version is %s'), $template ? $template->getVersion() : '')); } $fields->addFieldToTab('Root.Main', new WorkflowField( 'Workflow', _t('WorkflowDefinition.WORKFLOW', 'Workflow'), $this )); } else { // add in the 'template' info $templates = $this->workflowService->getTemplates(); if (is_array($templates)) { $items = array('' => ''); foreach ($templates as $template) { $items[$template->getName()] = $template->getName(); } $templates = array_combine(array_keys($templates), array_keys($templates)); $fields->addFieldToTab('Root.Main', $dd = new DropdownField('Template', _t('WorkflowDefinition.CHOOSE_TEMPLATE', 'Choose template (optional)'), $items)); $dd->setRightTitle(_t('WorkflowDefinition.CHOOSE_TEMPLATE_RIGHT', 'If set, this workflow definition will be automatically updated if the template is changed')); } /* * Uncomment to allow pre-uploaded exports to appear in a new DropdownField. * * $import = singleton('WorkflowDefinitionImporter')->getImportedWorkflows(); * if (is_array($import)) { * $_imports = array('' => ''); * foreach ($imports as $import) { * $_imports[$import->getName()] = $import->getName(); * } * $imports = array_combine(array_keys($_imports), array_keys($_imports)); * $fields->addFieldToTab('Root.Main', new DropdownField('Import', _t('WorkflowDefinition.CHOOSE_IMPORT', 'Choose import (optional)'), $imports)); * } */ $message = _t( 'WorkflowDefinition.ADDAFTERSAVING', 'You can add workflow steps after you save for the first time.' ); $fields->addFieldToTab('Root.Main', new LiteralField( 'AddAfterSaving', "<p class='message notice'>$message</p>" )); } if($this->ID && Permission::check('VIEW_ACTIVE_WORKFLOWS')) { $active = $this->Instances()->filter(array( 'WorkflowStatus' => array('Active', 'Paused') )); $active = new GridField( 'Active', _t('WorkflowDefinition.WORKFLOWACTIVEIINSTANCES', 'Active Workflow Instances'), $active, new GridFieldConfig_RecordEditor()); $active->getConfig()->removeComponentsByType('GridFieldAddNewButton'); $active->getConfig()->removeComponentsByType('GridFieldDeleteAction'); if(!Permission::check('REASSIGN_ACTIVE_WORKFLOWS')) { $active->getConfig()->removeComponentsByType('GridFieldEditButton'); $active->getConfig()->addComponent(new GridFieldViewButton()); $active->getConfig()->addComponent(new GridFieldDetailForm()); } $completed = $this->Instances()->filter(array( 'WorkflowStatus' => array('Complete', 'Cancelled') )); $config = new GridFieldConfig_Base(); $config->addComponent(new GridFieldEditButton()); $config->addComponent(new GridFieldDetailForm()); $completed = new GridField( 'Completed', _t('WorkflowDefinition.WORKFLOWCOMPLETEDIINSTANCES', 'Completed Workflow Instances'), $completed, $config); $fields->findOrMakeTab( 'Root.Active', _t('WorkflowEmbargoExpiryExtension.ActiveWorkflowStateTitle', 'Active') ); $fields->addFieldToTab('Root.Active', $active); $fields->findOrMakeTab( 'Root.Completed', _t('WorkflowEmbargoExpiryExtension.CompletedWorkflowStateTitle', 'Completed') ); $fields->addFieldToTab('Root.Completed', $completed); } $this->extend('updateCMSFields', $fields); return $fields; } public function updateAdminActions($actions) { if ($this->Template) { $template = $this->workflowService->getNamedTemplate($this->Template); if ($template && $this->TemplateVersion != $template->getVersion()) { $label = sprintf(_t('WorkflowDefinition.UPDATE_FROM_TEMLPATE', 'Update to latest template version (%s)'), $template->getVersion()); $actions->push($action = FormAction::create('updatetemplateversion', $label)); } } } public function updateFromTemplate() { if ($this->Template) { $template = $this->workflowService->getNamedTemplate($this->Template); $template->updateDefinition($this); } } /** * If a workflow-title doesn't already exist, we automatically create a suitable default title * when users attempt to create title-less workflow definitions or upload/create Workflows that would * otherwise have the same name. * * @return string * @todo Filter query on current-user's workflows. Avoids confusion when other users may already have 'My Workflow 1' * and user sees 'My Workflow 2' */ public function getDefaultWorkflowTitle() { // Where is the title coming from that we wish to test? $incomingTitle = $this->incomingTitle(); $defs = DataObject::get('WorkflowDefinition')->map()->toArray(); $tmp = array(); foreach($defs as $def) { $parts = preg_split("#\s#", $def, -1, PREG_SPLIT_NO_EMPTY); $lastPart = array_pop($parts); $match = implode(' ', $parts); // @todo do all this in one preg_match_all() call if(preg_match("#$match#", $incomingTitle)) { // @todo use a simple incrementer?? if($incomingTitle.' '.$lastPart == $def) { array_push($tmp, $lastPart); } } } $incr = 1; if(count($tmp)) { sort($tmp,SORT_NUMERIC); $incr = (int)end($tmp)+1; } return $incomingTitle.' '.$incr; } /** * Return the workflow definition title according to the source * * @return string */ public function incomingTitle() { $req = Controller::curr()->getRequest(); if(isset($req['_CsvFile']['name']) && !empty($req['_CsvFile']['name'])) { $import = DataObject::get('ImportedWorkflowTemplate')->filter('Filename', $req['_CsvFile']['name'])->first(); $incomingTitle = $import->Name; } else if(isset($req['Template']) && !empty($req['Template'])) { $incomingTitle = $req['Template']; } else if(isset($req['Title']) && !empty($req['Title'])) { $incomingTitle = $req['Title']; } else { $incomingTitle = self::$default_workflow_title_base; } return $incomingTitle; } /** * Determines if target can be published directly when no workflow has started yet * Opens extension hook to allow an extension to determine if this is allowed as well * * By default returns false * * @param $member * @param $target * @return Boolean */ public function canWorkflowPublish($member, $target) { $publish = $this->extendedCan('canWorkflowPublish', $member, $target); if (is_null($publish)) { return false; } return $publish; } /** * * @param Member $member * @param array $context * @return bool */ public function canCreate($member = null, $context = array()) { if (is_null($member)) { if (!Member::currentUserID()) { return false; } $member = Member::currentUser(); } return Permission::checkMember($member, 'CREATE_WORKFLOW'); } /** * * @param Member $member * @return boolean */ public function canView($member=null) { return $this->userHasAccess($member); } /** * * @param Member $member * @return boolean */ public function canEdit($member=null) { return $this->canCreate($member); } /** * * @param Member $member * @return boolean * @see {@link $this->onBeforeDelete()} */ public function canDelete($member = null) { if(!$member) { if(!Member::currentUserID()) { return false; } $member = Member::currentUser(); } if(Permission::checkMember($member, 'ADMIN')) { return true; } /* * DELETE_WORKFLOW should trump all other canDelete() return values on * related objects. * @see {@link $this->onBeforeDelete()} */ return Permission::checkMember($member, 'DELETE_WORKFLOW'); } /** * Checks whether the passed user is able to view this ModelAdmin * * @param Member $member * @return bool */ protected function userHasAccess($member) { if (!$member) { if (!Member::currentUserID()) { return false; } $member = Member::currentUser(); } if(Permission::checkMember($member, "VIEW_ACTIVE_WORKFLOWS")) { return true; } } } |