diff --git a/.travis.yml b/.travis.yml index 9cd09ae0..8ff0e64c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,30 @@ -# See https://github.com/silverstripe-labs/silverstripe-travis-support for setup details - language: php -php: - - 5.5 - env: - matrix: - - DB=MYSQL CORE_RELEASE=4 + global: + - COMPOSER_ROOT_VERSION="5.0.x-dev" matrix: include: - php: 5.6 - env: DB=MYSQL CORE_RELEASE=4 + env: DB=MYSQL PHPCS_TEST=1 PHPUNIT_TEST=1 + - php: 7.0 + env: DB=MYSQL PHPUNIT_TEST=1 + - php: 7.1 + env: DB=MYSQL PHPUNIT_COVERAGE_TEST=1 before_script: - - composer self-update || true - phpenv rehash - - git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support - - php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss - - cd ~/builds/ss + - phpenv config-rm xdebug.ini + + - composer install --prefer-dist + - composer require --prefer-dist --no-update silverstripe/recipe-cms:1.0.x-dev symbiote/silverstripe-queuedjobs:4.x-dev + - composer update script: - - vendor/bin/phpunit advancedworkflow/tests/ + - if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit; fi + - if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi + - if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=framework/phpcs.xml.dist code/ tests/ ; fi + +after_success: + - if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi diff --git a/.upgrade.yml b/.upgrade.yml new file mode 100644 index 00000000..3cdebd71 --- /dev/null +++ b/.upgrade.yml @@ -0,0 +1,39 @@ +mappings: + AssignUsersToWorkflowAction: Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction + CancelWorkflowAction: Symbiote\AdvancedWorkflow\Actions\CancelWorkflowAction + NotifyUsersWorkflowAction: Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction + PublishItemWorkflowAction: Symbiote\AdvancedWorkflow\Actions\PublishItemWorkflowAction + SetPropertyWorkflowAction: Symbiote\AdvancedWorkflow\Actions\SetPropertyWorkflowAction + SimpleApprovalWorkflowAction: Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction + UnpublishItemWorkflowAction: Symbiote\AdvancedWorkflow\Actions\UnpublishItemWorkflowAction + AdvancedWorkflowAdmin: Symbiote\AdvancedWorkflow\Admin\AdvancedWorkflowAdmin + WorkflowDefinitionItemRequestClass: Symbiote\AdvancedWorkflow\Admin\WorkflowDefinitionItemRequestClass + WorkflowDefinitionExporter: Symbiote\AdvancedWorkflow\Admin\WorkflowDefinitionExporter + WorkflowDefinitionImporter: Symbiote\AdvancedWorkflow\Admin\WorkflowDefinitionImporter + AdvancedWorkflowActionController: Symbiote\AdvancedWorkflow\Controllers\AdvancedWorkflowActionController + FrontEndWorkflowController: Symbiote\AdvancedWorkflow\Controllers\FrontEndWorkflowController + ImportedWorkflowTemplate: Symbiote\AdvancedWorkflow\DataObjects\ImportedWorkflowTemplate + WorkflowAction: Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction + WorkflowActionInstance: Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance + WorkflowDefinition: Symbiote\AdvancedWorkflow\DataObjects\WorkflowDefinition + WorkflowInstance: Symbiote\AdvancedWorkflow\DataObjects\WorkflowInstance + WorkflowTransition: Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition + WorkflowBulkLoader: Symbiote\AdvancedWorkflow\Dev\WorkflowBulkLoader + AdvancedWorkflowExtension: Symbiote\AdvancedWorkflow\Extensions\AdvancedWorkflowExtension + FileWorkflowApplicable: Symbiote\AdvancedWorkflow\Extensions\FileWorkflowApplicable + WorkflowApplicable: Symbiote\AdvancedWorkflow\Extensions\WorkflowApplicable + WorkflowEmbargoExpiryExtension: Symbiote\AdvancedWorkflow\Extensions\WorkflowEmbargoExpiryExtension + WorkflowField: Symbiote\AdvancedWorkflow\FormFields\WorkflowField + WorkflowFieldActionController: Symbiote\AdvancedWorkflow\FormFields\WorkflowFieldActionController + WorkflowFieldItemController: Symbiote\AdvancedWorkflow\FormFields\WorkflowFieldItemController + WorkflowFieldTransitionController: Symbiote\AdvancedWorkflow\FormFields\WorkflowFieldTransitionController + AWRequiredFields: Symbiote\AdvancedWorkflow\Forms\AWRequiredFields + FrontendWorkflowForm: Symbiote\AdvancedWorkflow\Forms\FrontendWorkflowForm + WorkflowPublishTargetJob: Symbiote\AdvancedWorkflow\Jobs\WorkflowPublishTargetJob + WorkflowReminderJob: Symbiote\AdvancedWorkflow\Jobs\WorkflowReminderJob + WorkflowService: Symbiote\AdvancedWorkflow\Services\WorkflowService + ExistingWorkflowException: Symbiote\AdvancedWorkflow\Services\ExistingWorkflowException + WorkflowReminderTask: Symbiote\AdvancedWorkflow\Tasks\WorkflowReminderTask + WorkflowTemplate: Symbiote\AdvancedWorkflow\Templates\WorkflowTemplate + GridFieldExportAction: Symbiote\AdvancedWorkflow\Forms\GridField\GridFieldExportAction + GridFieldWorkflowRestrictedEditButton: Symbiote\AdvancedWorkflow\Forms\GridField\GridFieldWorkflowRestrictedEditButton diff --git a/_config.php b/_config.php index fa9f2128..ec017173 100644 --- a/_config.php +++ b/_config.php @@ -3,11 +3,3 @@ * @license BSD License (http://silverstripe.org/bsd-license/) * @package advancedworkflow */ -define('ADVANCED_WORKFLOW_DIR', basename(dirname(__FILE__))); - -if(ADVANCED_WORKFLOW_DIR != 'advancedworkflow') { - throw new Exception( - "The advanced workflow module must be in a directory named 'advancedworkflow', not " . ADVANCED_WORKFLOW_DIR - ); -} - diff --git a/_config/workflowconfig.yml b/_config/workflowconfig.yml index cae77304..208584e7 100644 --- a/_config/workflowconfig.yml +++ b/_config/workflowconfig.yml @@ -1,28 +1,15 @@ --- Name: workflowconfig -After: - - 'framework/*' - - 'cms/*' --- -SiteTree: +SilverStripe\CMS\Model\SiteTree: extensions: - - WorkflowApplicable -CMSPageEditController: + - Symbiote\AdvancedWorkflow\Extensions\WorkflowApplicable +SilverStripe\CMS\Controllers\CMSPageEditController: extensions: - - AdvancedWorkflowExtension -GridFieldDetailForm_ItemRequest: + - Symbiote\AdvancedWorkflow\Extensions\AdvancedWorkflowExtension +SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest: extensions: - - AdvancedWorkflowExtension -LeftAndMain: + - Symbiote\AdvancedWorkflow\Extensions\AdvancedWorkflowExtension +SilverStripe\Admin\LeftAndMain: extra_requirements_css: - advancedworkflow/css/AdvancedWorkflowAdmin.css - ---- -Name: workflow_jobs -Only: - moduleexists: queuedjobs ---- -Injector: - WorkflowReminderJob: - properties: - queuedJobService: %$QueuedJobService diff --git a/_config/workflowjobs.yml b/_config/workflowjobs.yml new file mode 100644 index 00000000..905c77df --- /dev/null +++ b/_config/workflowjobs.yml @@ -0,0 +1,9 @@ +--- +Name: workflow_jobs +Only: + moduleexists: queuedjobs +--- +SilverStripe\Core\Injector\Injector: + Symbiote\AdvancedWorkflow\Jobs\WorkflowReminderJob: + properties: + queuedJobService: %$Symbiote\QueuedJobs\Services\QueuedJobService diff --git a/_config/workflows.yml b/_config/workflows.yml index f8780788..41f74dd4 100644 --- a/_config/workflows.yml +++ b/_config/workflows.yml @@ -1,49 +1,49 @@ --- Name: defaultworkflows --- -Injector: +SilverStripe\Core\Injector\Injector: SimpleReviewApprove: - class: WorkflowTemplate + class: Symbiote\AdvancedWorkflow\Templates\WorkflowTemplate constructor: - - Review and Approve - - Single step review and approve. Make sure to update the Apply for approval and Notify users steps! + - 'Review and Approve' + - 'Single step review and approve. Make sure to update the Apply for approval and Notify users steps!' - 0.2 properties: structure: - Apply for approval: - type: AssignUsersToWorkflowAction - transitions: - notify: Notify users - Notify users: - type: NotifyUsersWorkflowAction + 'Apply for approval': + type: Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction + transitions: + notify: 'Notify users' + 'Notify users': + type: Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction transitions: approval: Approval Approval: - type: SimpleApprovalWorkflowAction + type: Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction transitions: Approve: Publish - Reject: Reject changes + Reject: 'Reject changes' Publish: - type: PublishItemWorkflowAction - transitions: - assign: Assign Initiator Publish - Assign Initiator Publish: - type: AssignUsersToWorkflowAction - transitions: - notify: Notify Initiator Publish - Notify Initiator Publish: - type: NotifyUsersWorkflowAction - Reject changes: - type: CancelWorkflowAction - transitions: - assign: Assign Initiator Cancel - Assign Initiator Cancel: - type: AssignUsersToWorkflowAction - transitions: - notify: Notify Initiator Cancel - Notify Initiator Cancel: - type: NotifyUsersWorkflowAction - WorkflowService: + type: Symbiote\AdvancedWorkflow\Actions\PublishItemWorkflowAction + transitions: + assign: 'Assign Initiator Publish' + 'Assign Initiator Publish': + type: Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction + transitions: + notify: 'Notify Initiator Publish' + 'Notify Initiator Publish': + type: Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction + 'Reject changes': + type: Symbiote\AdvancedWorkflow\Actions\CancelWorkflowAction + transitions: + assign: 'Assign Initiator Cancel' + 'Assign Initiator Cancel': + type: Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction + transitions: + notify: 'Notify Initiator Cancel' + 'Notify Initiator Cancel': + type: Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction + Symbiote\AdvancedWorkflow\Services\WorkflowService: properties: templates: - - %$SimpleReviewApprove + - '%$SimpleReviewApprove' diff --git a/code/actions/AssignUsersToWorkflowAction.php b/code/actions/AssignUsersToWorkflowAction.php index d88a0b32..8636075d 100644 --- a/code/actions/AssignUsersToWorkflowAction.php +++ b/code/actions/AssignUsersToWorkflowAction.php @@ -1,8 +1,17 @@ 'Boolean', - ); - - private static $many_many = array( - 'Users' => 'SilverStripe\\Security\\Member', - 'Groups' => 'SilverStripe\\Security\\Group' - ); - - private static $icon = 'advancedworkflow/images/assign.png'; - - public function execute(WorkflowInstance $workflow) { - $workflow->Users()->removeAll(); - //Due to http://open.silverstripe.org/ticket/8258, there are errors occuring if Group has been extended - //We use a direct delete query here before ticket 8258 fixed - //$workflow->Groups()->removeAll(); - $workflowID = $workflow->ID; - $query = << 'Boolean', + ); + + private static $many_many = array( + 'Users' => Member::class, + 'Groups' => Group::class, + ); + + private static $icon = 'advancedworkflow/images/assign.png'; + + private static $table_name = 'AssignUsersToWorkflowAction'; + + public function execute(WorkflowInstance $workflow) + { + $workflow->Users()->removeAll(); + //Due to http://open.silverstripe.org/ticket/8258, there are errors occuring if Group has been extended + //We use a direct delete query here before ticket 8258 fixed + //$workflow->Groups()->removeAll(); + $workflowID = $workflow->ID; + $query = <<Users()->addMany($this->Users()); - $workflow->Groups()->addMany($this->Groups()); - if ($this->AssignInitiator) { - $workflow->Users()->add($workflow->Initiator()); - } - return true; - } - - public function getCMSFields() { - $fields = parent::getCMSFields(); - - $cmsUsers = Member::mapInCMSGroups(); - - $fields->addFieldsToTab('Root.Main', array( - new HeaderField('AssignUsers', $this->fieldLabel('AssignUsers')), - new CheckboxField('AssignInitiator', $this->fieldLabel('AssignInitiator')), - $users = CheckboxSetField::create('Users', $this->fieldLabel('Users'), $cmsUsers), - new TreeMultiselectField('Groups', $this->fieldLabel('Groups'), 'SilverStripe\\Security\\Group') - )); - - // limit to the users which actually can access the CMS - $users->setSource(Member::mapInCMSGroups()); - - return $fields; - } - - public function fieldLabels($relations = true) { - return array_merge(parent::fieldLabels($relations), array( - 'AssignUsers' => _t('AssignUsersToWorkflowAction.ASSIGNUSERS', 'Assign Users'), - 'Users' => _t('AssignUsersToWorkflowAction.USERS', 'Users'), - 'Groups' => _t('AssignUsersToWorkflowAction.GROUPS', 'Groups'), - 'AssignInitiator' => _t('AssignUsersToWorkflowAction.INITIATOR', 'Assign Initiator'), - )); - } - - /** - * Returns a set of all Members that are assigned to this WorkflowAction subclass, either directly or via a group. - * - * @return ArrayList - */ - public function getAssignedMembers() { - $members = $this->Users(); - $groups = $this->Groups(); - - // Can't merge instances of DataList so convert to something where we can - $_members = ArrayList::create(); - $members->each(function($item) use(&$_members) { - $_members->push($item); - }); - - $_groups = ArrayList::create(); - $groups->each(function($item) use(&$_groups) { - $_groups->push($item); - }); - - foreach($_groups as $group) { - $_members->merge($group->Members()); - } - - $_members->removeDuplicates(); - return $_members; - } + DB::query($query); + $workflow->Users()->addMany($this->Users()); + $workflow->Groups()->addMany($this->Groups()); + if ($this->AssignInitiator) { + $workflow->Users()->add($workflow->Initiator()); + } + return true; + } + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + $cmsUsers = Member::mapInCMSGroups(); + + $fields->addFieldsToTab('Root.Main', array( + new HeaderField('AssignUsers', $this->fieldLabel('AssignUsers')), + new CheckboxField('AssignInitiator', $this->fieldLabel('AssignInitiator')), + $users = CheckboxSetField::create('Users', $this->fieldLabel('Users'), $cmsUsers), + new TreeMultiselectField('Groups', $this->fieldLabel('Groups'), Group::class) + )); + + // limit to the users which actually can access the CMS + $users->setSource(Member::mapInCMSGroups()); + + return $fields; + } + + public function fieldLabels($relations = true) + { + return array_merge(parent::fieldLabels($relations), array( + 'AssignUsers' => _t('AssignUsersToWorkflowAction.ASSIGNUSERS', 'Assign Users'), + 'Users' => _t('AssignUsersToWorkflowAction.USERS', 'Users'), + 'Groups' => _t('AssignUsersToWorkflowAction.GROUPS', 'Groups'), + 'AssignInitiator' => _t('AssignUsersToWorkflowAction.INITIATOR', 'Assign Initiator'), + )); + } + + /** + * Returns a set of all Members that are assigned to this WorkflowAction subclass, either directly or via a group. + * + * @return ArrayList + */ + public function getAssignedMembers() + { + $members = $this->Users(); + $groups = $this->Groups(); + + // Can't merge instances of DataList so convert to something where we can + $_members = ArrayList::create(); + $members->each(function ($item) use ($_members) { + $_members->push($item); + }); + + $_groups = ArrayList::create(); + $groups->each(function ($item) use ($_groups) { + $_groups->push($item); + }); + + foreach ($_groups as $group) { + $_members->merge($group->Members()); + } + + $_members->removeDuplicates(); + return $_members; + } } diff --git a/code/actions/CancelWorkflowAction.php b/code/actions/CancelWorkflowAction.php index ddd21497..f0780a66 100644 --- a/code/actions/CancelWorkflowAction.php +++ b/code/actions/CancelWorkflowAction.php @@ -1,4 +1,9 @@ 'Varchar(100)', - 'EmailFrom' => 'Varchar(50)', - 'EmailTemplate' => 'Text' - ); - - private static $icon = 'advancedworkflow/images/notify.png'; - - public function getCMSFields() { - $fields = parent::getCMSFields(); - - $fields->addFieldsToTab('Root.Main', array( - new HeaderField('NotificationEmail', $this->fieldLabel('NotificationEmail')), - new LiteralField('NotificationNote', '

' . $this->fieldLabel('NotificationNote') . '

'), - new TextField('EmailSubject', $this->fieldLabel('EmailSubject')), - new TextField('EmailFrom', $this->fieldLabel('EmailFrom')), - - new TextareaField('EmailTemplate', $this->fieldLabel('EmailTemplate')), - new ToggleCompositeField('FormattingHelpContainer', - $this->fieldLabel('FormattingHelp'), new LiteralField('FormattingHelp', $this->getFormattingHelp())) - )); - - $this->extend('updateNotifyUsersCMSFields', $fields); - - return $fields; - } - - public function fieldLabels($relations = true) { - return array_merge(parent::fieldLabels($relations), array( - 'NotificationEmail' => _t('NotifyUsersWorkflowAction.NOTIFICATIONEMAIL', 'Notification Email'), - 'NotificationNote' => _t('NotifyUsersWorkflowAction.NOTIFICATIONNOTE', - 'All users attached to the workflow will be sent an email when this action is run.'), - 'EmailSubject' => _t('NotifyUsersWorkflowAction.EMAILSUBJECT', 'Email subject'), - 'EmailFrom' => _t('NotifyUsersWorkflowAction.EMAILFROM', 'Email from'), - 'EmailTemplate' => _t('NotifyUsersWorkflowAction.EMAILTEMPLATE', 'Email template'), - 'FormattingHelp' => _t('NotifyUsersWorkflowAction.FORMATTINGHELP', 'Formatting Help') - )); - } - - public function execute(WorkflowInstance $workflow) { - $members = $workflow->getAssignedMembers(); - - if(!$members || !count($members)) { - return true; - } - - $member = Member::currentUser(); - $initiator = $workflow->Initiator(); - - $contextFields = $this->getContextFields($workflow->getTarget()); - $memberFields = $this->getMemberFields($member); - $initiatorFields = $this->getMemberFields($initiator); - - $variables = array(); - - foreach($contextFields as $field => $val) $variables["\$Context.$field"] = $val; - foreach($memberFields as $field => $val) $variables["\$Member.$field"] = $val; - foreach($initiatorFields as $field => $val) $variables["\$Initiator.$field"] = $val; - - $pastActions = $workflow->Actions()->sort('Created DESC'); - $variables["\$CommentHistory"] = $this->customise(array( - 'PastActions'=>$pastActions, - 'Now'=>DBDatetime::now() - ))->renderWith('Includes/CommentHistory'); - - $from = str_replace(array_keys($variables), array_values($variables), $this->EmailFrom); - $subject = str_replace(array_keys($variables), array_values($variables), $this->EmailSubject); - - if ($this->config()->whitelist_template_variables) { - $item = new ArrayData(array( - 'Initiator' => new ArrayData($initiatorFields), - 'Member' => new ArrayData($memberFields), - 'Context' => new ArrayData($contextFields), - 'CommentHistory' => $variables["\$CommentHistory"] - )); - } - else { - $item = $workflow->customise(array( - 'Items' => $workflow->Actions(), - 'Member' => $member, - 'Context' => new ArrayData($contextFields), - 'CommentHistory' => $variables["\$CommentHistory"] - )); - } - - - $view = SSViewer::fromString($this->EmailTemplate); - $this->extend('updateView', $view); - - $body = $view->process($item); - - foreach($members as $member) { - if($member->Email) { - $email = new Email; - $email->setTo($member->Email); - $email->setSubject($subject); - $email->setFrom($from); - $email->setBody($body); - $email->send(); - } - } - - return true; - } - - /** - * @param DataObject $target - * @return array - */ - public function getContextFields(DataObject $target) { - $result = array(); - if (!$target) { - return $result; - } - - $fields = $target->db(); +class NotifyUsersWorkflowAction extends WorkflowAction +{ + /** + * @config + * @var bool Should templates be constrained to just known-safe variables. + */ + private static $whitelist_template_variables = false; + + private static $db = array( + 'EmailSubject' => 'Varchar(100)', + 'EmailFrom' => 'Varchar(50)', + 'EmailTemplate' => 'Text' + ); + + private static $icon = 'advancedworkflow/images/notify.png'; + + private static $table_name = 'NotifyUsersWorkflowAction'; + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + $fields->addFieldsToTab('Root.Main', array( + new HeaderField('NotificationEmail', $this->fieldLabel('NotificationEmail')), + new LiteralField('NotificationNote', '

' . $this->fieldLabel('NotificationNote') . '

'), + new TextField('EmailSubject', $this->fieldLabel('EmailSubject')), + new TextField('EmailFrom', $this->fieldLabel('EmailFrom')), + + new TextareaField('EmailTemplate', $this->fieldLabel('EmailTemplate')), + new ToggleCompositeField( + 'FormattingHelpContainer', + $this->fieldLabel('FormattingHelp'), + new LiteralField('FormattingHelp', $this->getFormattingHelp()) + ) + )); + + $this->extend('updateNotifyUsersCMSFields', $fields); + + return $fields; + } + + public function fieldLabels($relations = true) + { + return array_merge(parent::fieldLabels($relations), array( + 'NotificationEmail' => _t('NotifyUsersWorkflowAction.NOTIFICATIONEMAIL', 'Notification Email'), + 'NotificationNote' => _t( + 'NotifyUsersWorkflowAction.NOTIFICATIONNOTE', + 'All users attached to the workflow will be sent an email when this action is run.' + ), + 'EmailSubject' => _t('NotifyUsersWorkflowAction.EMAILSUBJECT', 'Email subject'), + 'EmailFrom' => _t('NotifyUsersWorkflowAction.EMAILFROM', 'Email from'), + 'EmailTemplate' => _t('NotifyUsersWorkflowAction.EMAILTEMPLATE', 'Email template'), + 'FormattingHelp' => _t('NotifyUsersWorkflowAction.FORMATTINGHELP', 'Formatting Help') + )); + } + + public function execute(WorkflowInstance $workflow) + { + $members = $workflow->getAssignedMembers(); + + if (!$members || !count($members)) { + return true; + } + + $member = Security::getCurrentUser(); + $initiator = $workflow->Initiator(); + + $contextFields = $this->getContextFields($workflow->getTarget()); + $memberFields = $this->getMemberFields($member); + $initiatorFields = $this->getMemberFields($initiator); + + $variables = array(); + + foreach ($contextFields as $field => $val) { + $variables["\$Context.$field"] = $val; + } + foreach ($memberFields as $field => $val) { + $variables["\$Member.$field"] = $val; + } + foreach ($initiatorFields as $field => $val) { + $variables["\$Initiator.$field"] = $val; + } + + $pastActions = $workflow->Actions()->sort('Created DESC'); + $variables["\$CommentHistory"] = $this->customise(array( + 'PastActions'=>$pastActions, + 'Now'=>DBDatetime::now() + ))->renderWith('Includes/CommentHistory'); + + $from = str_replace(array_keys($variables), array_values($variables), $this->EmailFrom); + $subject = str_replace(array_keys($variables), array_values($variables), $this->EmailSubject); + + if ($this->config()->whitelist_template_variables) { + $item = new ArrayData(array( + 'Initiator' => new ArrayData($initiatorFields), + 'Member' => new ArrayData($memberFields), + 'Context' => new ArrayData($contextFields), + 'CommentHistory' => $variables["\$CommentHistory"] + )); + } else { + $item = $workflow->customise(array( + 'Items' => $workflow->Actions(), + 'Member' => $member, + 'Context' => new ArrayData($contextFields), + 'CommentHistory' => $variables["\$CommentHistory"] + )); + } + + + $view = SSViewer::fromString($this->EmailTemplate); + $this->extend('updateView', $view); + + $body = $view->process($item); + + foreach ($members as $member) { + if ($member->Email) { + $email = new Email; + $email->setTo($member->Email); + $email->setSubject($subject); + $email->setFrom($from); + $email->setBody($body); + $email->send(); + } + } + + return true; + } + + /** + * @param DataObject $target + * @return array + */ + public function getContextFields(DataObject $target) + { + $result = array(); + if (!$target) { + return $result; + } + + $fields = $target->db(); unset($fields['ID']); - foreach($fields as $field => $fieldDesc) { - $result[$field] = $target->$field; - } - - if($target instanceof CMSPreviewable) { - $result['CMSLink'] = $target->CMSEditLink(); - } else if ($target->hasMethod('WorkflowLink')) { - $result['CMSLink'] = $target->WorkflowLink(); - } - - return $result; - } - - /** - * Builds an array with the member information - * @param Member $member An optional member to use. If null, will use the current logged in member - * @return array - */ - public function getMemberFields(Member $member = null) { - if (!$member){ - $member = Member::currentUser(); - } - $result = array(); - - if($member) foreach($member->summaryFields() as $field => $title) { - $result[$field] = $member->$field; - } - - if($member && !array_key_exists('Name', $result)) { - $result['Name'] = $member->getName(); - } - - return $result; - } - - - /** - * Returns a basic set of instructions on how email templates are populated with variables. - * - * @return string - */ - public function getFormattingHelp() { - $note = _t('NotifyUsersWorkflowAction.FORMATTINGNOTE', - 'Notification emails can contain HTML formatting. The following special variables are replaced with their - respective values in the email subject, email from and template/body.'); - $member = _t('NotifyUsersWorkflowAction.MEMBERNOTE', - 'These fields will be populated from the member that initiates the notification action. For example, - {$Member.FirstName}.'); - $initiator = _t('NotifyUsersWorkflowAction.INITIATORNOTE', - 'These fields will be populated from the member that initiates the workflow request. For example, - {$Initiator.Email}.'); - $context = _t('NotifyUsersWorkflowAction.CONTEXTNOTE', - 'Any summary fields from the workflow target will be available. For example, {$Context.Title}. + foreach ($fields as $field => $fieldDesc) { + $result[$field] = $target->$field; + } + + if ($target instanceof CMSPreviewable) { + $result['CMSLink'] = $target->CMSEditLink(); + } elseif ($target->hasMethod('WorkflowLink')) { + $result['CMSLink'] = $target->WorkflowLink(); + } + + return $result; + } + + /** + * Builds an array with the member information + * @param Member $member An optional member to use. If null, will use the current logged in member + * @return array + */ + public function getMemberFields(Member $member = null) + { + if (!$member) { + $member = Security::getCurrentUser(); + } + $result = array(); + + if ($member) { + foreach ($member->summaryFields() as $field => $title) { + $result[$field] = $member->$field; + } + } + + if ($member && !array_key_exists('Name', $result)) { + $result['Name'] = $member->getName(); + } + + return $result; + } + + + /** + * Returns a basic set of instructions on how email templates are populated with variables. + * + * @return string + */ + public function getFormattingHelp() + { + $note = _t( + 'NotifyUsersWorkflowAction.FORMATTINGNOTE', + 'Notification emails can contain HTML formatting. The following special variables are replaced with their + respective values in the email subject, email from and template/body.' + ); + $member = _t( + 'NotifyUsersWorkflowAction.MEMBERNOTE', + 'These fields will be populated from the member that initiates the notification action. For example, + {$Member.FirstName}.' + ); + $initiator = _t( + 'NotifyUsersWorkflowAction.INITIATORNOTE', + 'These fields will be populated from the member that initiates the workflow request. For example, + {$Initiator.Email}.' + ); + $context = _t( + 'NotifyUsersWorkflowAction.CONTEXTNOTE', + 'Any summary fields from the workflow target will be available. For example, {$Context.Title}. Additionally, the {$Context.AbsoluteEditLink} variable will contain a link to edit the workflow target in the CMS (if it is a Page), and the {$Context.LinkToPendingItems} variable will generate a link to the CMS\' workflow admin, - useful for allowing users to enact workflow transitions, directly from emails.'); - $fieldName = _t('NotifyUsersWorkflowAction.FIELDNAME', 'Field name'); - $commentHistory = _t('NotifyUsersWorkflowAction.COMMENTHISTORY', 'Comment history up to this notification.'); + useful for allowing users to enact workflow transitions, directly from emails.' + ); + $fieldName = _t('NotifyUsersWorkflowAction.FIELDNAME', 'Field name'); + $commentHistory = _t('NotifyUsersWorkflowAction.COMMENTHISTORY', 'Comment history up to this notification.'); - $memberFields = implode(', ', array_keys($this->getMemberFields())); + $memberFields = implode(', ', array_keys($this->getMemberFields())); - return "

$note

+ return "

$note

{\$Member.($memberFields)}
$member

{\$Initiator.($memberFields)}
$initiator

{\$Context.($fieldName)}
$context

{\$CommentHistory}
$commentHistory

"; - } - + } } diff --git a/code/actions/PublishItemWorkflowAction.php b/code/actions/PublishItemWorkflowAction.php index 6e8fba84..a41736df 100644 --- a/code/actions/PublishItemWorkflowAction.php +++ b/code/actions/PublishItemWorkflowAction.php @@ -1,6 +1,19 @@ 'Int', +class PublishItemWorkflowAction extends WorkflowAction +{ + private static $db = array( + 'PublishDelay' => 'Int', 'AllowEmbargoedEditing' => 'Boolean', - ); + ); private static $defaults = array( 'AllowEmbargoedEditing' => true ); - private static $icon = 'advancedworkflow/images/publish.png'; + private static $icon = 'advancedworkflow/images/publish.png'; + + private static $table_name = 'PublishItemWorkflowAction'; - public function execute(WorkflowInstance $workflow) { - if (!$target = $workflow->getTarget()) { - return true; - } + public function execute(WorkflowInstance $workflow) + { + if (!$target = $workflow->getTarget()) { + return true; + } - if (class_exists('AbstractQueuedJob') && $this->PublishDelay) { - $job = new WorkflowPublishTargetJob($target); - $days = $this->PublishDelay; - $after = date('Y-m-d H:i:s', strtotime("+$days days")); + if (class_exists(AbstractQueuedJob::class) && $this->PublishDelay) { + $job = new WorkflowPublishTargetJob($target); + $days = $this->PublishDelay; + $after = date('Y-m-d H:i:s', strtotime("+$days days")); // disable editing, and embargo the delay if using WorkflowEmbargoExpiryExtension - if ($target->hasExtension('WorkflowEmbargoExpiryExtension')) { + if ($target->hasExtension(WorkflowEmbargoExpiryExtension::class)) { $target->AllowEmbargoedEditing = $this->AllowEmbargoedEditing; $target->PublishOnDate = $after; $target->write(); } else { - singleton('QueuedJobService')->queueJob($job, $after); + singleton(QueuedJobService::class)->queueJob($job, $after); } - } else if ($target->hasExtension('WorkflowEmbargoExpiryExtension')) { + } elseif ($target->hasExtension(WorkflowEmbargoExpiryExtension::class)) { $target->AllowEmbargoedEditing = $this->AllowEmbargoedEditing; - // setting future date stuff if needbe - - // set this value regardless - $target->UnPublishOnDate = $target->DesiredUnPublishDate; - $target->DesiredUnPublishDate = ''; - if ($target->DesiredPublishDate) { - $target->PublishOnDate = $target->DesiredPublishDate; - $target->DesiredPublishDate = ''; - $target->write(); - } else { - if ($target->hasMethod('doPublish')) { - $target->doPublish(); - } - } - } else { - if ($target->hasMethod('doPublish')) { - $target->doPublish(); - } - } - - return true; - } - - public function getCMSFields() { - $fields = parent::getCMSFields(); - - if (class_exists('AbstractQueuedJob')) { - $before = _t('PublishItemWorkflowAction.DELAYPUBDAYSBEFORE', 'Delay publication '); - $after = _t('PublishItemWorkflowAction.DELAYPUBDAYSAFTER', ' days'); - $allowEmbargoed = _t('PublishItemWorkflowAction.ALLOWEMBARGOEDEDITING', - 'Allow editing while item is embargoed? (does not apply without embargo)'); - - $fields->addFieldsToTab('Root.Main', array( + // setting future date stuff if needbe + + // set this value regardless + $target->UnPublishOnDate = $target->DesiredUnPublishDate; + $target->DesiredUnPublishDate = ''; + + // Publish dates + if ($target->DesiredPublishDate) { + // Hand-off desired publish date + $target->PublishOnDate = $target->DesiredPublishDate; + $target->DesiredPublishDate = ''; + $target->write(); + } else { + // Ensure previously modified DesiredUnPublishDate values are written + $target->write(); + if ($target->hasMethod('publishSingle')) { + $target->publishSingle(); + } + } + } else { + if ($target->hasMethod('publishSingle')) { + $target->publishSingle(); + } + } + + return true; + } + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + if (class_exists(AbstractQueuedJob::class)) { + $before = _t('PublishItemWorkflowAction.DELAYPUBDAYSBEFORE', 'Delay publication '); + $after = _t('PublishItemWorkflowAction.DELAYPUBDAYSAFTER', ' days'); + $allowEmbargoed = _t( + 'PublishItemWorkflowAction.ALLOWEMBARGOEDEDITING', + 'Allow editing while item is embargoed? (does not apply without embargo)' + ); + + $fields->addFieldsToTab('Root.Main', array( new CheckboxField('AllowEmbargoedEditing', $allowEmbargoed), new FieldGroup( _t('PublishItemWorkflowAction.PUBLICATIONDELAY', 'Publication Delay'), @@ -83,19 +107,19 @@ public function getCMSFields() { new LabelField('PublishDelayAfter', $after) ), )); - } - - return $fields; - } + } - /** - * Publish action allows a user who is currently assigned at this point of the workflow to - * - * @param DataObject $target - * @return bool - */ - public function canPublishTarget(DataObject $target) { - return true; - } + return $fields; + } + /** + * Publish action allows a user who is currently assigned at this point of the workflow to + * + * @param DataObject $target + * @return bool + */ + public function canPublishTarget(DataObject $target) + { + return true; + } } diff --git a/code/actions/SetPropertyWorkflowAction.php b/code/actions/SetPropertyWorkflowAction.php index 7bb475db..bf5b36f1 100644 --- a/code/actions/SetPropertyWorkflowAction.php +++ b/code/actions/SetPropertyWorkflowAction.php @@ -1,40 +1,50 @@ */ -class SetPropertyWorkflowAction extends WorkflowAction { - private static $db = array( - 'Property' => 'Varchar', - 'Value' => 'Text', - ); - - public function execute(WorkflowInstance $workflow) { - if (!$target = $workflow->getTarget()) { - return true; - } - - if ($target->hasField($this->Property)) { - $target->setField($this->Property, $this->Value); - } - - $target->write(); - - return true; - } - - public function getCMSFields() { - $fields = parent::getCMSFields(); - - $fields->addFieldsToTab('Root.Main', array( - TextField::create('Property', _t('SetPropertyWorkflowAction.PROPERTY', 'Property')) - ->setRightTitle(_t('SetPropertyWorkflowAction.PROPERTYTITLE', 'Property to set; if this exists as a setter method, will be called passing the value')), - TextField::create('Value', 'Value') - )); - - return $fields; - } - +class SetPropertyWorkflowAction extends WorkflowAction +{ + private static $db = array( + 'Property' => 'Varchar', + 'Value' => 'Text', + ); + + private static $table_name = 'SetPropertyWorkflowAction'; + + public function execute(WorkflowInstance $workflow) + { + if (!$target = $workflow->getTarget()) { + return true; + } + + if ($target->hasField($this->Property)) { + $target->setField($this->Property, $this->Value); + } + + $target->write(); + + return true; + } + + public function getCMSFields() + { + $fields = parent::getCMSFields(); + + $fields->addFieldsToTab('Root.Main', array( + TextField::create('Property', _t('SetPropertyWorkflowAction.PROPERTY', 'Property')) + ->setRightTitle(_t('SetPropertyWorkflowAction.PROPERTYTITLE', 'Property to set; if this exists as a setter method, will be called passing the value')), + TextField::create('Value', 'Value') + )); + + return $fields; + } } diff --git a/code/actions/SimpleApprovalWorkflowAction.php b/code/actions/SimpleApprovalWorkflowAction.php index f5793e8c..ebc63d73 100644 --- a/code/actions/SimpleApprovalWorkflowAction.php +++ b/code/actions/SimpleApprovalWorkflowAction.php @@ -1,4 +1,10 @@ 'Int' - ); +class UnpublishItemWorkflowAction extends WorkflowAction +{ + private static $db = array( + 'UnpublishDelay' => 'Int' + ); - private static $icon = 'advancedworkflow/images/unpublish.png'; + private static $icon = 'advancedworkflow/images/unpublish.png'; - public function execute(WorkflowInstance $workflow) { - if (!$target = $workflow->getTarget()) { - return true; - } + private static $table_name = 'UnpublishItemWorkflowAction'; - if (class_exists('AbstractQueuedJob') && $this->UnpublishDelay) { - $job = new WorkflowPublishTargetJob($target, "unpublish"); - $days = $this->UnpublishDelay; - $after = date('Y-m-d H:i:s', strtotime("+$days days")); - singleton('QueuedJobService')->queueJob($job, $after); - } else if ($target->hasExtension('WorkflowEmbargoExpiryExtension')) { - // setting future date stuff if needbe + public function execute(WorkflowInstance $workflow) + { + if (!$target = $workflow->getTarget()) { + return true; + } - // set these values regardless - $target->DesiredUnPublishDate = ''; - $target->DesiredPublishDate = ''; - $target->write(); + if (class_exists(AbstractQueuedJob::class) && $this->UnpublishDelay) { + $job = new WorkflowPublishTargetJob($target, "unpublish"); + $days = $this->UnpublishDelay; + $after = date('Y-m-d H:i:s', strtotime("+$days days")); + singleton(QueuedJobService::class)->queueJob($job, $after); + } elseif ($target->hasExtension(WorkflowEmbargoExpiryExtension::class)) { + // setting future date stuff if needbe - if ($target->hasMethod('doUnpublish')) { - $target->doUnpublish(); - } - } else { - if ($target->hasMethod('doUnpublish')) { - $target->doUnpublish(); - } - } + // set these values regardless + $target->DesiredUnPublishDate = ''; + $target->DesiredPublishDate = ''; + $target->write(); - return true; - } + if ($target->hasMethod('doUnpublish')) { + $target->doUnpublish(); + } + } else { + if ($target->hasMethod('doUnpublish')) { + $target->doUnpublish(); + } + } - public function getCMSFields() { - $fields = parent::getCMSFields(); + return true; + } - if (class_exists('AbstractQueuedJob')) { - $before = _t('UnpublishItemWorkflowAction.DELAYUNPUBDAYSBEFORE', 'Delay unpublishing by '); - $after = _t('UnpublishItemWorkflowAction.DELAYUNPUBDAYSAFTER', ' days'); + public function getCMSFields() + { + $fields = parent::getCMSFields(); - $fields->addFieldToTab('Root.Main', new FieldGroup( - _t('UnpublishItemWorkflowAction.UNPUBLICATIONDELAY', 'Delay Un-publishing'), - new LabelField('UnpublishDelayBefore', $before), - new NumericField('UnpublishDelay', ''), - new LabelField('UnpublishDelayAfter', $after) - )); - } + if (class_exists(AbstractQueuedJob::class)) { + $before = _t('UnpublishItemWorkflowAction.DELAYUNPUBDAYSBEFORE', 'Delay unpublishing by '); + $after = _t('UnpublishItemWorkflowAction.DELAYUNPUBDAYSAFTER', ' days'); - return $fields; - } + $fields->addFieldToTab('Root.Main', new FieldGroup( + _t('UnpublishItemWorkflowAction.UNPUBLICATIONDELAY', 'Delay Un-publishing'), + new LabelField('UnpublishDelayBefore', $before), + new NumericField('UnpublishDelay', ''), + new LabelField('UnpublishDelayAfter', $after) + )); + } - /** - * @param DataObject $target - * @return bool - */ - public function canPublishTarget(DataObject $target) { - return false; - } + return $fields; + } + /** + * @param DataObject $target + * @return bool + */ + public function canPublishTarget(DataObject $target) + { + return false; + } } diff --git a/code/admin/AdvancedWorkflowAdmin.php b/code/admin/AdvancedWorkflowAdmin.php index cb233450..d67289ca 100644 --- a/code/admin/AdvancedWorkflowAdmin.php +++ b/code/admin/AdvancedWorkflowAdmin.php @@ -1,326 +1,350 @@ text) */ -class AdvancedWorkflowAdmin extends ModelAdmin { - - private static $menu_title = 'Workflows'; - private static $menu_priority = -1; - private static $url_segment = 'workflows'; - private static $menu_icon = "advancedworkflow/images/workflow-menu-icon.png"; - - /** - * - * @var array Allowable actions on this controller. - */ - private static $allowed_actions = array( - 'export', - 'ImportForm' - ); - - private static $url_handlers = array( - '$ModelClass/export/$ID!' => 'export', - '$ModelClass/$Action' => 'handleAction', - '' => 'index' - ); - - private static $managed_models = 'WorkflowDefinition'; - - private static $model_importers = array( - 'WorkflowDefinition' => 'WorkflowBulkLoader' - ); - - private static $dependencies = array( - 'workflowService' => '%$WorkflowService', - ); - - private static $fileEditActions = 'getCMSActions'; - - /** - * Defaults are set in {@link getEditForm()}. - * - * @var array - */ - private static $fieldOverrides = array(); - - /** - * @var WorkflowService - */ - public $workflowService; - - /** - * Initialise javascript translation files - * - * @return void - */ - public function init() { - parent::init(); - Requirements::add_i18n_javascript('advancedworkflow/javascript/lang'); - Requirements::javascript('advancedworkflow/javascript/WorkflowField.js'); - Requirements::javascript('advancedworkflow/javascript/WorkflowGridField.js'); - Requirements::css('advancedworkflow/css/WorkflowField.css'); - Requirements::css('advancedworkflow/css/WorkflowGridField.css'); - } - - /* +class AdvancedWorkflowAdmin extends ModelAdmin +{ + private static $menu_title = 'Workflows'; + private static $menu_priority = -1; + private static $url_segment = 'workflows'; + private static $menu_icon = "advancedworkflow/images/workflow-menu-icon.png"; + + /** + * + * @var array Allowable actions on this controller. + */ + private static $allowed_actions = array( + 'export', + 'ImportForm' + ); + + private static $url_handlers = array( + '$ModelClass/export/$ID!' => 'export', + '$ModelClass/$Action' => 'handleAction', + '' => 'index' + ); + + private static $managed_models = WorkflowDefinition::class; + + private static $model_importers = array( + 'WorkflowDefinition' => WorkflowBulkLoader::class + ); + + private static $dependencies = array( + 'workflowService' => '%$' . WorkflowService::class, + ); + + private static $fileEditActions = 'getCMSActions'; + + /** + * Defaults are set in {@link getEditForm()}. + * + * @var array + */ + private static $fieldOverrides = array(); + + /** + * @var WorkflowService + */ + public $workflowService; + + /** + * Initialise javascript translation files + * + * @return void + */ + protected function init() + { + parent::init(); + + $module = ModuleLoader::getModule('symbiote/silverstripe-advancedworkflow'); + + Requirements::add_i18n_javascript($module->getRelativeResourcePath('/javascript/lang')); + Requirements::javascript($module->getRelativeResourcePath('/javascript/WorkflowField.js')); + Requirements::javascript($module->getRelativeResourcePath('/javascript/WorkflowGridField.js')); + Requirements::css($module->getRelativeResourcePath('/css/WorkflowField.css')); + Requirements::css($module->getRelativeResourcePath('/css/WorkflowGridField.css')); + } + + /* * Shows up to x2 GridFields for Pending and Submitted items, dependent upon the current CMS user and that user's permissions * on the objects showing in each field. */ - public function getEditForm($id = null, $fields = null) { - $form = parent::getEditForm($id, $fields); - - // Show items submitted into a workflow for current user to action - $fieldName = 'PendingObjects'; - $pending = $this->userObjects(Member::currentUser(), $fieldName); - - if($this->config()->fieldOverrides) { - $displayFields = $this->config()->fieldOverrides; - } else { - $displayFields = array( - 'Title' => _t('AdvancedWorkflowAdmin.Title', 'Title'), - 'LastEdited' => _t('AdvancedWorkflowAdmin.LastEdited', 'Changed'), - 'WorkflowTitle' => _t('AdvancedWorkflowAdmin.WorkflowTitle', 'Effective workflow'), - 'WorkflowStatus' => _t('AdvancedWorkflowAdmin.WorkflowStatus', 'Current action'), - ); - } - - // Pending/Submitted items GridField Config - $config = new GridFieldConfig_Base(); - $config->addComponent(new GridFieldEditButton()); - $config->addComponent(new GridFieldDetailForm()); - $config->getComponentByType('GridFieldPaginator')->setItemsPerPage(5); - $columns = $config->getComponentByType('GridFieldDataColumns'); - $columns->setFieldFormatting($this->setFieldFormatting($config)); - - if($pending->count()) { - $formFieldTop = GridField::create( - $fieldName, - $this->isAdminUser(Member::currentUser())? - _t( - 'AdvancedWorkflowAdmin.GridFieldTitleAssignedAll', - 'All pending items' - ): - _t( - 'AdvancedWorkflowAdmin.GridFieldTitleAssignedYour', - 'Your pending items'), - $pending, - $config - ); - - $dataColumns = $formFieldTop->getConfig()->getComponentByType('GridFieldDataColumns'); - $dataColumns->setDisplayFields($displayFields); - - $formFieldTop->setForm($form); - $form->Fields()->insertBefore($formFieldTop, 'WorkflowDefinition'); - } - - // Show items submitted into a workflow by current user - $fieldName = 'SubmittedObjects'; - $submitted = $this->userObjects(Member::currentUser(), $fieldName); - if($submitted->count()) { - $formFieldBottom = GridField::create( - $fieldName, - $this->isAdminUser(Member::currentUser())? - _t( - 'AdvancedWorkflowAdmin.GridFieldTitleSubmittedAll', - 'All submitted items' - ): - _t( - 'AdvancedWorkflowAdmin.GridFieldTitleSubmittedYour', - 'Your submitted items'), - $submitted, - $config - ); - - $dataColumns = $formFieldBottom->getConfig()->getComponentByType('GridFieldDataColumns'); - $dataColumns->setDisplayFields($displayFields); - - $formFieldBottom->setForm($form); - $formFieldBottom->getConfig()->removeComponentsByType('GridFieldEditButton'); - $formFieldBottom->getConfig()->addComponent(new GridFieldWorkflowRestrictedEditButton()); - $form->Fields()->insertBefore($formFieldBottom, 'WorkflowDefinition'); - } - - $grid = $form->Fields()->dataFieldByName('WorkflowDefinition'); - if ($grid) { - $grid->getConfig()->getComponentByType('GridFieldDetailForm')->setItemEditFormCallback(function ($form) { - $record = $form->getRecord(); - if ($record) { - $record->updateAdminActions($form->Actions()); - } - }); - - $grid->getConfig()->getComponentByType('GridFieldDetailForm')->setItemRequestClass('WorkflowDefinitionItemRequestClass'); - $grid->getConfig()->addComponent(new GridFieldExportAction()); - $grid->getConfig()->removeComponentsByType('GridFieldExportButton'); - } - - return $form; - } - - /* + public function getEditForm($id = null, $fields = null) + { + $form = parent::getEditForm($id, $fields); + + // Show items submitted into a workflow for current user to action + $fieldName = 'PendingObjects'; + $pending = $this->userObjects(Member::currentUser(), $fieldName); + + if ($this->config()->fieldOverrides) { + $displayFields = $this->config()->fieldOverrides; + } else { + $displayFields = array( + 'Title' => _t('AdvancedWorkflowAdmin.Title', 'Title'), + 'LastEdited' => _t('AdvancedWorkflowAdmin.LastEdited', 'Changed'), + 'WorkflowTitle' => _t('AdvancedWorkflowAdmin.WorkflowTitle', 'Effective workflow'), + 'WorkflowStatus' => _t('AdvancedWorkflowAdmin.WorkflowStatus', 'Current action'), + ); + } + + // Pending/Submitted items GridField Config + $config = new GridFieldConfig_Base(); + $config->addComponent(new GridFieldEditButton()); + $config->addComponent(new GridFieldDetailForm()); + $config->getComponentByType(GridFieldPaginator::class)->setItemsPerPage(5); + $columns = $config->getComponentByType(GridFieldDataColumns::class); + $columns->setFieldFormatting($this->setFieldFormatting($config)); + + if ($pending->count()) { + $formFieldTop = GridField::create( + $fieldName, + $this->isAdminUser(Member::currentUser())? + _t( + 'AdvancedWorkflowAdmin.GridFieldTitleAssignedAll', + 'All pending items' + ): + _t( + 'AdvancedWorkflowAdmin.GridFieldTitleAssignedYour', + 'Your pending items' + ), + $pending, + $config + ); + + $dataColumns = $formFieldTop->getConfig()->getComponentByType(GridFieldDataColumns::class); + $dataColumns->setDisplayFields($displayFields); + + $formFieldTop->setForm($form); + $form->Fields()->insertBefore($formFieldTop, WorkflowDefinition::class); + } + + // Show items submitted into a workflow by current user + $fieldName = 'SubmittedObjects'; + $submitted = $this->userObjects(Member::currentUser(), $fieldName); + if ($submitted->count()) { + $formFieldBottom = GridField::create( + $fieldName, + $this->isAdminUser(Member::currentUser())? + _t( + 'AdvancedWorkflowAdmin.GridFieldTitleSubmittedAll', + 'All submitted items' + ): + _t( + 'AdvancedWorkflowAdmin.GridFieldTitleSubmittedYour', + 'Your submitted items' + ), + $submitted, + $config + ); + + $dataColumns = $formFieldBottom->getConfig()->getComponentByType(GridFieldDataColumns::class); + $dataColumns->setDisplayFields($displayFields); + + $formFieldBottom->setForm($form); + $formFieldBottom->getConfig()->removeComponentsByType(GridFieldEditButton::class); + $formFieldBottom->getConfig()->addComponent(new GridFieldWorkflowRestrictedEditButton()); + $form->Fields()->insertBefore($formFieldBottom, WorkflowDefinition::class); + } + + $grid = $form->Fields()->dataFieldByName(WorkflowDefinition::class); + if ($grid) { + $grid->getConfig()->getComponentByType(GridFieldDetailForm::class)->setItemEditFormCallback(function ($form) { + $record = $form->getRecord(); + if ($record) { + $record->updateAdminActions($form->Actions()); + } + }); + + $grid->getConfig()->getComponentByType(GridFieldDetailForm::class)->setItemRequestClass(WorkflowDefinitionItemRequestClass::class); + $grid->getConfig()->addComponent(new GridFieldExportAction()); + $grid->getConfig()->removeComponentsByType(GridFieldExportButton::class); + } + + return $form; + } + + /* * @param Member $user * @return boolean */ - public function isAdminUser(Member $user) { - if(Permission::checkMember($user, 'ADMIN')) { - return true; - } - return false; - } - - /* + public function isAdminUser(Member $user) + { + if (Permission::checkMember($user, 'ADMIN')) { + return true; + } + return false; + } + + /* * By default, we implement GridField_ColumnProvider to allow users to click through to the PagesAdmin. * We would also like a "Quick View", that allows users to quickly make a decision on a given workflow-bound content-object */ - public function columns() { - $fields = array( - 'Title' => array( - 'link' => function($value, $item) { - $pageAdminLink = singleton('CMSPageEditController')->Link('show'); - return sprintf('%s',$pageAdminLink,$item->Link,$value); - } - ), - 'WorkflowStatus' => array( - 'text' => function($value, $item) { - return $item->WorkflowCurrentAction; - } - ) - ); - return $fields; - } - - /* + public function columns() + { + $fields = array( + 'Title' => array( + 'link' => function ($value, $item) { + $pageAdminLink = singleton(CMSPageEditController::class)->Link('show'); + return sprintf('%s', $pageAdminLink, $item->Link, $value); + } + ), + 'WorkflowStatus' => array( + 'text' => function ($value, $item) { + return $item->WorkflowCurrentAction; + } + ) + ); + return $fields; + } + + /* * Discreet method used by both intro gridfields to format the target object's links and clickable text * * @param GridFieldConfig $config * @return array $fieldFormatting */ - public function setFieldFormatting(&$config) { - $fieldFormatting = array(); - // Parse the column information - foreach($this->columns() as $source => $info) { - if(isset($info['link']) && $info['link']) { - $fieldFormatting[$source] = '$value'; - } - if(isset($info['text']) && $info['text']) { - $fieldFormatting[$source] = $info['text']; - } - } - return $fieldFormatting; - } - - /** - * Get WorkflowInstance Target objects to show for users in initial gridfield(s) - * - * @param Member $member - * @param string $fieldName The name of the gridfield that determines which dataset to return - * @return DataList - * @todo Add the ability to see embargo/expiry dates in report-gridfields at-a-glance if QueuedJobs module installed - */ - public function userObjects(Member $user, $fieldName) { - $list = new ArrayList(); - $userWorkflowInstances = $this->getFieldDependentData($user, $fieldName); - foreach($userWorkflowInstances as $instance) { - if(!$instance->TargetID || !$instance->DefinitionID) { - continue; - } - // @todo can we use $this->getDefinitionFor() to fetch the "Parent" definition of $instance? Maybe define $this->workflowParent() - $effectiveWorkflow = DataObject::get_by_id('WorkflowDefinition', $instance->DefinitionID); - $target = $instance->getTarget(); - if(!is_object($effectiveWorkflow) || !$target) { - continue; - } - $instance->setField('WorkflowTitle',$effectiveWorkflow->getField('Title')); - $instance->setField('WorkflowCurrentAction',$instance->getCurrentAction()); - // Note the order of property-setting here, somehow $instance->Title is overwritten by the Target Title property.. - $instance->setField('Title',$target->getField('Title')); - $instance->setField('LastEdited',$target->getField('LastEdited')); - if (method_exists($target, 'CMSEditLink')) { - $instance->setField('ObjectRecordLink', $target->CMSEditLink()); - } - - $list->push($instance); - } - return $list; - } - - /* + public function setFieldFormatting(&$config) + { + $fieldFormatting = array(); + // Parse the column information + foreach ($this->columns() as $source => $info) { + if (isset($info['link']) && $info['link']) { + $fieldFormatting[$source] = '$value'; + } + if (isset($info['text']) && $info['text']) { + $fieldFormatting[$source] = $info['text']; + } + } + return $fieldFormatting; + } + + /** + * Get WorkflowInstance Target objects to show for users in initial gridfield(s) + * + * @param Member $member + * @param string $fieldName The name of the gridfield that determines which dataset to return + * @return DataList + * @todo Add the ability to see embargo/expiry dates in report-gridfields at-a-glance if QueuedJobs module installed + */ + public function userObjects(Member $user, $fieldName) + { + $list = new ArrayList(); + $userWorkflowInstances = $this->getFieldDependentData($user, $fieldName); + foreach ($userWorkflowInstances as $instance) { + if (!$instance->TargetID || !$instance->DefinitionID) { + continue; + } + // @todo can we use $this->getDefinitionFor() to fetch the "Parent" definition of $instance? Maybe define $this->workflowParent() + $effectiveWorkflow = DataObject::get_by_id(WorkflowDefinition::class, $instance->DefinitionID); + $target = $instance->getTarget(); + if (!is_object($effectiveWorkflow) || !$target) { + continue; + } + $instance->setField('WorkflowTitle', $effectiveWorkflow->getField('Title')); + $instance->setField('WorkflowCurrentAction', $instance->getCurrentAction()); + // Note the order of property-setting here, somehow $instance->Title is overwritten by the Target Title property.. + $instance->setField('Title', $target->getField('Title')); + $instance->setField('LastEdited', $target->getField('LastEdited')); + if (method_exists($target, 'CMSEditLink')) { + $instance->setField('ObjectRecordLink', $target->CMSEditLink()); + } + + $list->push($instance); + } + return $list; + } + + /* * Return content-object data depending on which gridfeld is calling for it * * @param Member $user * @param string $fieldName */ - public function getFieldDependentData(Member $user, $fieldName) { - if($fieldName == 'PendingObjects') { - return $this->workflowService->userPendingItems($user); - } - if($fieldName == 'SubmittedObjects') { - return $this->workflowService->userSubmittedItems($user); - } - } - - /** - * Spits out an exported version of the selected WorkflowDefinition for download. - * - * @param \SS_HTTPRequest $request - * @return \SS_HTTPResponse - */ - public function export(SS_HTTPRequest $request) { - $url = explode('/', $request->getURL()); - $definitionID = end($url); - if($definitionID && is_numeric($definitionID)) { - $exporter = new WorkflowDefinitionExporter($definitionID); - $exportFilename = WorkflowDefinitionExporter::$export_filename_prefix.'-'.$definitionID.'.yml'; - $exportBody = $exporter->export(); - $fileData = array( - 'name' => $exportFilename, - 'mime' => 'text/x-yaml', - 'body' => $exportBody, - 'size' => $exporter->getExportSize($exportBody) - ); - return $exporter->sendFile($fileData); - } - } - - /** - * Required so we can simply change the visible label of the "Import" button and lose some redundant form-fields. - * - * @return Form - */ - public function ImportForm() { - $form = parent::ImportForm(); - if(!$form) { - return; - } - - $form->unsetAllActions(); - $newActionList = new FieldList(array( - new FormAction('import', _t('AdvancedWorkflowAdmin.IMPORT', 'Import workflow')) - )); - $form->Fields()->fieldByName('_CsvFile')->getValidator()->setAllowedExtensions(array('yml', 'yaml')); - $form->Fields()->removeByName('EmptyBeforeImport'); - $form->setActions($newActionList); - - return $form; - } -} + public function getFieldDependentData(Member $user, $fieldName) + { + if ($fieldName == 'PendingObjects') { + return $this->workflowService->userPendingItems($user); + } + if ($fieldName == 'SubmittedObjects') { + return $this->workflowService->userSubmittedItems($user); + } + } + + /** + * Spits out an exported version of the selected WorkflowDefinition for download. + * + * @param HTTPRequest $request + * @return HTTPResponse + */ + public function export(HTTPRequest $request) + { + $url = explode('/', $request->getURL()); + $definitionID = end($url); + if ($definitionID && is_numeric($definitionID)) { + $exporter = new WorkflowDefinitionExporter($definitionID); + $exportFilename = WorkflowDefinitionExporter::$export_filename_prefix.'-'.$definitionID.'.yml'; + $exportBody = $exporter->export(); + $fileData = array( + 'name' => $exportFilename, + 'mime' => 'text/x-yaml', + 'body' => $exportBody, + 'size' => $exporter->getExportSize($exportBody) + ); + return $exporter->sendFile($fileData); + } + } + + /** + * Required so we can simply change the visible label of the "Import" button and lose some redundant form-fields. + * + * @return Form + */ + public function ImportForm() + { + $form = parent::ImportForm(); + if (!$form) { + return; + } -class WorkflowDefinitionItemRequestClass extends GridFieldDetailForm_ItemRequest { - public function updatetemplateversion($data, Form $form, $request) { - $record = $form->getRecord(); - if ($record) { - $record->updateFromTemplate(); - } - return $form->loadDataFrom($form->getRecord())->forAjaxTemplate(); - } -} \ No newline at end of file + $form->unsetAllActions(); + $newActionList = new FieldList(array( + new FormAction('import', _t('AdvancedWorkflowAdmin.IMPORT', 'Import workflow')) + )); + $form->Fields()->fieldByName('_CsvFile')->getValidator()->setAllowedExtensions(array('yml', 'yaml')); + $form->Fields()->removeByName('EmptyBeforeImport'); + $form->setActions($newActionList); + + return $form; + } +} diff --git a/code/admin/WorkflowDefinitionExporter.php b/code/admin/WorkflowDefinitionExporter.php index 1a6fc830..8ac5ea89 100644 --- a/code/admin/WorkflowDefinitionExporter.php +++ b/code/admin/WorkflowDefinitionExporter.php @@ -1,8 +1,19 @@ setMember(Member::currentUser()); - $this->workflowDefinition = DataObject::get_by_id('WorkflowDefinition', $definitionID); - } + /** + * The base filename of the file to the exported + * + * @config + * @var string + */ + private static $export_filename_prefix = 'workflow-definition-export'; + /** + * + * @var Member + */ + protected $member; + /** + * + * @var WorkflowDefinition + */ + protected $workflowDefinition; - /** - * - * @param \Member $member - */ - public function setMember($member) { - $this->member = $member; - } + /** + * + * @param number $definitionID + * @return void + */ + public function __construct($definitionID) + { + $this->setMember(Security::getCurrentUser()); + $this->workflowDefinition = DataObject::get_by_id(WorkflowDefinition::class, $definitionID); + } - /** - * @return \WorkflowDefinition - */ - public function getDefinition() { - return $this->workflowDefinition; - } + /** + * + * @param Member $member + */ + public function setMember($member) + { + $this->member = $member; + } - /** - * Runs the export - * - * @return string $template - */ - public function export() { - // Disable any access to use of WorkflowExport if user has no SecurityAdmin access - if(!Permission::check('CMS_ACCESS_SecurityAdmin')) { - throw Exception(_t('ErrorPage.403'), 403); - } - $def = $this->getDefinition(); - $templateData = new ArrayData(array( - 'ExportMetaData' => $this->ExportMetaData(), - 'ExportActions' => $def->Actions(), - 'ExportUsers' => $def->Users(), - 'ExportGroups' => $def->Groups() - )); - return $this->format($templateData); - } + /** + * @return WorkflowDefinition + */ + public function getDefinition() + { + return $this->workflowDefinition; + } - /** - * Format the exported data as YAML. - * - * @param \ArrayData $templateData - * @return void - */ - public function format($templateData) { - $viewer = SSViewer::execute_template(['type' => 'Includes', 'WorkflowDefinitionExport'], $templateData); - // Temporary until we find the source of the replacement in SSViewer - $processed = str_replace('&', '&', $viewer); - // Clean-up newline "gaps" that SSViewer leaves behind from the placement of template control structures - return preg_replace("#^\R+|^[\t\s]*\R+#m", '', $processed); - } + /** + * Runs the export + * + * @return string $template + */ + public function export() + { + // Disable any access to use of WorkflowExport if user has no SecurityAdmin access + if (!Permission::check('CMS_ACCESS_SecurityAdmin')) { + throw Exception(_t('ErrorPage.403'), 403); + } + $def = $this->getDefinition(); + $templateData = new ArrayData(array( + 'ExportMetaData' => $this->ExportMetaData(), + 'ExportActions' => $def->Actions(), + 'ExportUsers' => $def->Users(), + 'ExportGroups' => $def->Groups() + )); + return $this->format($templateData); + } - /** - * Returns the size of the current export in bytes. - * Used for pushing data to the browser to prompt for download - * - * @param string $str - * @return number $bytes - */ - public function getExportSize($str) { - return mb_strlen($str, 'UTF-8'); - } + /** + * Format the exported data as YAML. + * + * @param ArrayData $templateData + * @return void + */ + public function format($templateData) + { + $viewer = SSViewer::execute_template(['type' => 'Includes', 'WorkflowDefinitionExport'], $templateData); + // Temporary until we find the source of the replacement in SSViewer + $processed = str_replace('&', '&', $viewer); + // Clean-up newline "gaps" that SSViewer leaves behind from the placement of template control structures + return preg_replace("#^\R+|^[\t\s]*\R+#m", '', $processed); + } - /** - * Generate template vars for metadata - * - * @return ArrayData - */ - public function ExportMetaData() { - $def = $this->getDefinition(); - return new ArrayData(array( - 'ExportHost' => preg_replace("#http(s)?://#", '', Director::protocolAndHost()), - 'ExportDate' => date('d/m/Y H-i-s'), - 'ExportUser' => $this->member->FirstName.' '.$this->member->Surname, - 'ExportVersionFramework' => $this->ssVersion(), - 'ExportWorkflowDefName' => $this->processTitle($def->Title), - 'ExportRemindDays' => $def->RemindDays, - 'ExportSort' => $def->Sort - )); - } + /** + * Returns the size of the current export in bytes. + * Used for pushing data to the browser to prompt for download + * + * @param string $str + * @return number $bytes + */ + public function getExportSize($str) + { + return mb_strlen($str, 'UTF-8'); + } - /* - * Try different ways of obtaining the current SilverStripe version for YAML output. - * - * @return string - */ - private function ssVersion() { - // Remove colons so they don't screw with YAML parsing - $versionSapphire = str_replace(':', '', singleton('SapphireInfo')->Version()); - $versionLeftMain = str_replace(':', '', singleton('LeftAndMain')->CMSVersion()); - if($versionSapphire != _t('LeftAndMain.VersionUnknown')) { - return $versionSapphire; - } - return $versionLeftMain; - } + /** + * Generate template vars for metadata + * + * @return ArrayData + */ + public function ExportMetaData() + { + $def = $this->getDefinition(); + return new ArrayData(array( + 'ExportHost' => preg_replace("#http(s)?://#", '', Director::protocolAndHost()), + 'ExportDate' => date('d/m/Y H-i-s'), + 'ExportUser' => $this->member->FirstName.' '.$this->member->Surname, + 'ExportVersionFramework' => $this->ssVersion(), + 'ExportWorkflowDefName' => $this->processTitle($def->Title), + 'ExportRemindDays' => $def->RemindDays, + 'ExportSort' => $def->Sort + )); + } - private function processTitle($title) { - // If an import is exported and re-imported, the new export date is appended to Title, making for a very long title - return preg_replace("#\s[\d]+\/[\d]+\/[\d]+\s[\d]+-[\d]+-[\d]+(\s[\d]+)?#", '', $title); - } + /** + * Try different ways of obtaining the current SilverStripe version for YAML output. + * + * @return string + */ + private function ssVersion() + { + // Remove colons so they don't screw with YAML parsing + $versionSapphire = str_replace(':', '', singleton(SapphireInfo::class)->Version()); + $versionLeftMain = str_replace(':', '', singleton(LeftAndMain::class)->CMSVersion()); + if ($versionSapphire != _t('SilverStripe\\Admin\\LeftAndMain.VersionUnknown', 'Unknown')) { + return $versionSapphire; + } + return $versionLeftMain; + } - /** - * Prompt the client for file download. - * We're "overriding" SS_HTTPRequest::send_file() for more robust cross-browser support - * - * @param array $filedata - * @return \SS_HTTPResponse $response - */ - public function sendFile($filedata) { - $response = new SS_HTTPResponse($filedata['body']); - if(preg_match("#MSIE\s(6|7|8)?\.0#",$_SERVER['HTTP_USER_AGENT'])) { - // IE headers - $response->addHeader("Cache-Control","public"); - $response->addHeader("Content-Disposition","attachment; filename=\"".basename($filedata['name'])."\""); - $response->addHeader("Content-Type","application/force-download"); - $response->addHeader("Content-Type","application/octet-stream"); - $response->addHeader("Content-Type","application/download"); - $response->addHeader("Content-Type",$filedata['mime']); - $response->addHeader("Content-Description","File Transfer"); - $response->addHeader("Content-Length",$filedata['size']); - } - else { - // Everyone else - $response->addHeader("Content-Type", $filedata['mime']."; name=\"".addslashes($filedata['name'])."\""); - $response->addHeader("Content-disposition", "attachment; filename=".addslashes($filedata['name'])); - $response->addHeader("Content-Length",$filedata['size']); - } - return $response; - } + private function processTitle($title) + { + // If an import is exported and re-imported, the new export date is appended to Title, making for a very long title + return preg_replace("#\s[\d]+\/[\d]+\/[\d]+\s[\d]+-[\d]+-[\d]+(\s[\d]+)?#", '', $title); + } + /** + * Prompt the client for file download. + * We're "overriding" SS_HTTPRequest::send_file() for more robust cross-browser support + * + * @param array $filedata + * @return HTTPResponse $response + */ + public function sendFile($filedata) + { + $response = new HTTPResponse($filedata['body']); + if (preg_match("#MSIE\s(6|7|8)?\.0#", $_SERVER['HTTP_USER_AGENT'])) { + // IE headers + $response->addHeader("Cache-Control", "public"); + $response->addHeader("Content-Disposition", "attachment; filename=\"".basename($filedata['name'])."\""); + $response->addHeader("Content-Type", "application/force-download"); + $response->addHeader("Content-Type", "application/octet-stream"); + $response->addHeader("Content-Type", "application/download"); + $response->addHeader("Content-Type", $filedata['mime']); + $response->addHeader("Content-Description", "File Transfer"); + $response->addHeader("Content-Length", $filedata['size']); + } else { + // Everyone else + $response->addHeader("Content-Type", $filedata['mime']."; name=\"".addslashes($filedata['name'])."\""); + $response->addHeader("Content-disposition", "attachment; filename=".addslashes($filedata['name'])); + $response->addHeader("Content-Length", $filedata['size']); + } + return $response; + } } diff --git a/code/admin/WorkflowDefinitionImporter.php b/code/admin/WorkflowDefinitionImporter.php new file mode 100644 index 00000000..86eee31c --- /dev/null +++ b/code/admin/WorkflowDefinitionImporter.php @@ -0,0 +1,89 @@ +Content) { + continue; + } + $structure = unserialize($import->Content); + $struct = $structure[Injector::class]['ExportedWorkflow']; + $template = Injector::inst()->createWithArgs(WorkflowTemplate::class, $struct['constructor']); + $template->setStructure($struct['properties']['structure']); + if ($name) { + if ($struct['constructor'][0] == trim($name)) { + return $template; + } + continue; + } + $importedDefs[] = $template; + } + return $importedDefs; + } + + /** + * Handles finding and parsing YAML input as a string or from the contents of a file. + * + * @see addYAMLConfigFile() on {@link SS_ConfigManifest} from where this logic was taken and adapted. + * @param string $source YAML as a string or a filename + * @return array + */ + public function parseYAMLImport($source) + { + if (is_file($source)) { + $source = file_get_contents($source); + } + + // Make sure the linefeeds are all converted to \n, PCRE '$' will not match anything else. + $convertLF = str_replace(array("\r\n", "\r"), "\n", $source); + /* + * Remove illegal colons from Transition/Action titles, otherwise sfYamlParser will barf on them + * Note: The regex relies on there being single quotes wrapped around these in the export .ss template + */ + $converted = preg_replace("#('[^:\n][^']+)(:)([^']+')#", "$1;$3", $convertLF); + $parts = preg_split('#^---$#m', $converted, -1, PREG_SPLIT_NO_EMPTY); + + // If we got an odd number of parts the config, file doesn't have a header. + // We know in advance the number of blocks imported content will have so we settle for a count()==2 check. + if (count($parts) != 2) { + $msg = _t('WorkflowDefinitionImporter.INVALID_YML_FORMAT_NO_HEADER', 'Invalid YAML format.'); + throw new ValidationException($msg); + } + + try { + $parsed = Yaml::parse($parts[1]); + return $parsed; + } catch (Exception $e) { + $msg = _t('WorkflowDefinitionImporter.INVALID_YML_FORMAT_NO_PARSE', 'Invalid YAML format. Unable to parse.'); + throw new ValidationException($msg); + } + } +} diff --git a/code/admin/WorkflowDefinitionItemRequestClass.php b/code/admin/WorkflowDefinitionItemRequestClass.php new file mode 100644 index 00000000..652d7b49 --- /dev/null +++ b/code/admin/WorkflowDefinitionItemRequestClass.php @@ -0,0 +1,18 @@ +getRecord(); + if ($record) { + $record->updateFromTemplate(); + } + return $form->loadDataFrom($form->getRecord())->forAjaxTemplate(); + } +} diff --git a/code/admin/WorklowDefinitionImporter.php b/code/admin/WorklowDefinitionImporter.php deleted file mode 100644 index 8e1ae341..00000000 --- a/code/admin/WorklowDefinitionImporter.php +++ /dev/null @@ -1,82 +0,0 @@ -Content) { - continue; - } - $structure = unserialize($import->Content); - $struct = $structure['Injector']['ExportedWorkflow']; - $template = Injector::inst()->createWithArgs('WorkflowTemplate', $struct['constructor']); - $template->setStructure($struct['properties']['structure']); - if($name) { - if($struct['constructor'][0] == trim($name)) { - return $template; - } - continue; - } - $importedDefs[] = $template; - } - return $importedDefs; - } - - /** - * Handles finding and parsing YAML input as a string or from the contents of a file. - * - * @see addYAMLConfigFile() on {@link SS_ConfigManifest} from where this logic was taken and adapted. - * @param string $source YAML as a string or a filename - * @return array - */ - public function parseYAMLImport($source) { - if(is_file($source)) { - $source = file_get_contents($source); - } - - require_once('thirdparty/zend_translate_railsyaml/library/Translate/Adapter/thirdparty/sfYaml/lib/sfYamlParser.php'); - $parser = new sfYamlParser(); - - // Make sure the linefeeds are all converted to \n, PCRE '$' will not match anything else. - $convertLF = str_replace(array("\r\n", "\r"), "\n", $source); - /* - * Remove illegal colons from Transition/Action titles, otherwise sfYamlParser will barf on them - * Note: The regex relies on there being single quotes wrapped around these in the export .ss template - */ - $converted = preg_replace("#('[^:\n][^']+)(:)([^']+')#", "$1;$3", $convertLF); - $parts = preg_split('#^---$#m', $converted, -1, PREG_SPLIT_NO_EMPTY); - - // If we got an odd number of parts the config, file doesn't have a header. - // We know in advance the number of blocks imported content will have so we settle for a count()==2 check. - if(count($parts) != 2) { - $msg = _t('WorkflowDefinitionImporter.INVALID_YML_FORMAT_NO_HEADER', 'Invalid YAML format.'); - throw new ValidationException($msg); - } - - try { - $parsed = $parser->parse($parts[1]); - return $parsed; - } catch (Exception $e) { - $msg = _t('WorkflowDefinitionImporter.INVALID_YML_FORMAT_NO_PARSE', 'Invalid YAML format. Unable to parse.'); - throw new ValidationException($msg); - } - } -} diff --git a/code/controllers/AdvancedWorkflowActionController.php b/code/controllers/AdvancedWorkflowActionController.php index 3e5f59d9..8cab2de8 100644 --- a/code/controllers/AdvancedWorkflowActionController.php +++ b/code/controllers/AdvancedWorkflowActionController.php @@ -1,9 +1,15 @@ request->requestVar('id'); - $transition = $this->request->requestVar('transition'); - - $instance = DataObject::get_by_id('WorkflowInstance', (int) $id); - if ($instance && $instance->canEdit()) { - $transition = DataObject::get_by_id('WorkflowTransition', (int) $transition); - if ($transition) { - if ($this->request->requestVar('comments')) { - $action = $instance->CurrentAction(); - $action->Comment = $this->request->requestVar('comments'); - $action->write(); - } - - singleton('WorkflowService')->executeTransition($instance->getTarget(), $transition->ID); - $result = array( - 'success' => true, - 'link' => $instance->getTarget()->AbsoluteLink() - ); - if (Director::is_ajax()) { - return Convert::raw2json($result); - } else { - return $this->redirect($instance->getTarget()->Link()); - } - } - } - - if (Director::is_ajax()) { - $result = array( - 'success' => false, - ); - return Convert::raw2json($result); - } else { - $this->redirect($instance->getTarget()->Link()); - } - } +class AdvancedWorkflowActionController extends Controller +{ + public function transition($request) + { + if (!Security::getCurrentUser()) { + return Security::permissionFailure( + $this, + _t( + 'AdvancedWorkflowActionController.ACTION_ERROR', + "You must be logged in" + ) + ); + } + + $id = $this->request->requestVar('id'); + $transition = $this->request->requestVar('transition'); + + $instance = DataObject::get_by_id(WorkflowInstance::class, (int) $id); + if ($instance && $instance->canEdit()) { + $transition = DataObject::get_by_id(WorkflowTransition::class, (int) $transition); + if ($transition) { + if ($this->request->requestVar('comments')) { + $action = $instance->CurrentAction(); + $action->Comment = $this->request->requestVar('comments'); + $action->write(); + } + + singleton(WorkflowService::class)->executeTransition($instance->getTarget(), $transition->ID); + $result = array( + 'success' => true, + 'link' => $instance->getTarget()->AbsoluteLink() + ); + if (Director::is_ajax()) { + return Convert::raw2json($result); + } + return $this->redirect($instance->getTarget()->Link()); + } + } + + if (Director::is_ajax()) { + $result = array( + 'success' => false, + ); + return Convert::raw2json($result); + } + + return $this->redirect($instance->getTarget()->Link()); + } } diff --git a/code/controllers/FrontEndWorkflowController.php b/code/controllers/FrontEndWorkflowController.php index 01b9fb1b..8ebfbd9b 100644 --- a/code/controllers/FrontEndWorkflowController.php +++ b/code/controllers/FrontEndWorkflowController.php @@ -1,200 +1,213 @@ contextObj) { - if ($id = $this->getContextID()) { - $cType = $this->getContextType(); - $cObj = DataObject::get_by_id($cType, $id); - if ($cObj) { - $this->contextObj = $cObj->canView() ? $cObj : null; - } - } - } - return $this->contextObj; - } - - /** - * @return int ID of Context Object - */ - protected function getContextID() { - $id = $this->contextObj ? $this->contextObj->ID : null; - if (!$id) { - if ($this->request->param('ID')) { - $id = (int) $this->request->param('ID'); - } else if ($this->request->requestVar('ID')) { - $id = (int) $this->request->requestVar('ID'); - } - } - return $id; - } - - /** - * Specifies the Workflow Definition to be used, - * ie. retrieve from SiteConfig - or wherever it's defined - * - * @return WorkflowDefinition - */ - abstract function getWorkflowDefinition(); - - /** - * Handle the Form Action - * - FrontEndWorkflowForm contains the logic for this - * - * @param SS_HTTPRequest $request - * @todo - is this even required??? - */ - public function handleAction($request, $action){ - return parent::handleAction($request, $action); - } - - /** - * Create the Form containing: - * - fields from the Context Object - * - required fields from the Context Object - * - Actions from the connected WorkflowTransitions - * - * @return Form - */ - public function Form(){ - - $svc = singleton('WorkflowService'); - $active = $svc->getWorkflowFor($this->getContextObject()); - - if (!$active){ - return; - //throw new Exception('Workflow not found, or not specified for Context Object'); - } - - $wfFields = $active->getFrontEndWorkflowFields(); - $wfActions = $active->getFrontEndWorkflowActions(); - $wfValidator = $active->getFrontEndRequiredFields(); - - //Get DataObject for Form (falls back to ContextObject if not defined in WorkflowAction) - $wfDataObject = $active->getFrontEndDataObject(); - - // set any requirements spcific to this contextobject - $active->setFrontendFormRequirements(); - - // hooks for decorators - $this->extend('updateFrontEndWorkflowFields', $wfFields); - $this->extend('updateFrontEndWorkflowActions', $wfActions); - $this->extend('updateFrontEndRequiredFields', $wfValidator); - $this->extend('updateFrontendFormRequirements'); - - $form = new FrontendWorkflowForm($this, 'Form/' . $this->getContextID(), $wfFields, $wfActions, $wfValidator); - - $form->addExtraClass("fwf"); - - if($wfDataObject) { - $form->loadDataFrom($wfDataObject); - } - - return $form; - } - - /** - * @return WorkflowTransition - */ - public function getCurrentTransition() { - $trans = null; - if ($this->transitionID) { - $trans = DataObject::get_by_id('WorkflowTransition',$this->transitionID); - } - return $trans; - } - - /** - * Save the Form Data to the defined Context Object - * - * @param array $data - * @param Form $form - * @param SS_HTTPRequest $request - * @throws Exception - */ - public function doFrontEndAction(array $data, Form $form, SS_HTTPRequest $request) { - if (!$obj = $this->getContextObject()) { - throw new Exception( - _t( - 'FrontEndWorkflowController.FRONTENDACTION_CONTEXT_EXCEPTION', - 'Context Object Not Found' - ) - ); - } - - if(!$this->getCurrentTransition()->canExecute($this->contextObj->getWorkflowInstance())){ - throw new Exception( - _t( - 'FrontEndWorkflowController.FRONTENDACTION_TRANSITION_EXCEPTION', - 'You do not have permission to execute this action' - ) - ); - } - - //Only Save data when Transition is 'Active' - if ($this->getCurrentTransition()->Type == 'Active') { - //Hand off to WorkflowAction to perform Save - $svc = singleton('WorkflowService'); - $active = $svc->getWorkflowFor($obj); - - $active->doFrontEndAction($data, $form, $request); - } - - //run execute on WorkflowInstance instance - $action = $this->contextObj->getWorkflowInstance()->currentAction(); - $action->BaseAction()->execute($this->contextObj->getWorkflowInstance()); - - //get valid transitions - $transitions = $action->getValidTransitions(); - - //tell instance to execute transition if it's in the permitted list - if ($transitions->find('ID',$this->transitionID)) { - $this->contextObj->getWorkflowInstance()->performTransition($this->getCurrentTransition()); - } - } - - /** - * checks to see if there is a title set on the current workflow action - * uses that or falls back to controller->Title - */ - public function Title(){ - if (!$this->Title) { - if($this->getContextObject()){ - if($workflow = $this->contextObj->getWorkflowInstance()){ - $this->Title = $workflow->currentAction()->BaseAction()->PageTitle ? $workflow->currentAction()->BaseAction()->PageTitle : $workflow->currentAction()->Title; - } - } - } - return $this->Title; - } - -} \ No newline at end of file +abstract class FrontEndWorkflowController extends Controller +{ + protected $transitionID; + protected $contextObj; + + /** + * The title to be displayed on the page + * @var string + */ + public $Title; + + + /** + * @return string ClassName of object that Workflow is applied to + */ + abstract public function getContextType(); + + /** + * @return object Context Object + */ + public function getContextObject() + { + if (!$this->contextObj) { + if ($id = $this->getContextID()) { + $cType = $this->getContextType(); + $cObj = DataObject::get_by_id($cType, $id); + if ($cObj) { + $this->contextObj = $cObj->canView() ? $cObj : null; + } + } + } + return $this->contextObj; + } + + /** + * @return int ID of Context Object + */ + protected function getContextID() + { + $id = $this->contextObj ? $this->contextObj->ID : null; + if (!$id) { + if ($this->request->param('ID')) { + $id = (int) $this->request->param('ID'); + } elseif ($this->request->requestVar('ID')) { + $id = (int) $this->request->requestVar('ID'); + } + } + return $id; + } + + /** + * Specifies the Workflow Definition to be used, + * ie. retrieve from SiteConfig - or wherever it's defined + * + * @return WorkflowDefinition + */ + abstract public function getWorkflowDefinition(); + + /** + * Handle the Form Action + * - FrontEndWorkflowForm contains the logic for this + * + * @param SS_HTTPRequest $request + * @todo - is this even required??? + */ + public function handleAction($request, $action) + { + return parent::handleAction($request, $action); + } + + /** + * Create the Form containing: + * - fields from the Context Object + * - required fields from the Context Object + * - Actions from the connected WorkflowTransitions + * + * @return Form + */ + public function Form() + { + $svc = singleton(WorkflowService::class); + $active = $svc->getWorkflowFor($this->getContextObject()); + + if (!$active) { + return; + //throw new Exception('Workflow not found, or not specified for Context Object'); + } + + $wfFields = $active->getFrontEndWorkflowFields(); + $wfActions = $active->getFrontEndWorkflowActions(); + $wfValidator = $active->getFrontEndRequiredFields(); + + //Get DataObject for Form (falls back to ContextObject if not defined in WorkflowAction) + $wfDataObject = $active->getFrontEndDataObject(); + + // set any requirements spcific to this contextobject + $active->setFrontendFormRequirements(); + + // hooks for decorators + $this->extend('updateFrontEndWorkflowFields', $wfFields); + $this->extend('updateFrontEndWorkflowActions', $wfActions); + $this->extend('updateFrontEndRequiredFields', $wfValidator); + $this->extend('updateFrontendFormRequirements'); + + $form = new FrontendWorkflowForm($this, 'Form/' . $this->getContextID(), $wfFields, $wfActions, $wfValidator); + + $form->addExtraClass("fwf"); + + if ($wfDataObject) { + $form->loadDataFrom($wfDataObject); + } + + return $form; + } + + /** + * @return WorkflowTransition + */ + public function getCurrentTransition() + { + $trans = null; + if ($this->transitionID) { + $trans = DataObject::get_by_id(WorkflowTransition::class, $this->transitionID); + } + return $trans; + } + + /** + * Save the Form Data to the defined Context Object + * + * @param array $data + * @param Form $form + * @param SS_HTTPRequest $request + * @throws Exception + */ + public function doFrontEndAction(array $data, Form $form, HTTPRequest $request) + { + if (!$obj = $this->getContextObject()) { + throw new Exception( + _t( + 'FrontEndWorkflowController.FRONTENDACTION_CONTEXT_EXCEPTION', + 'Context Object Not Found' + ) + ); + } + + if (!$this->getCurrentTransition()->canExecute($this->contextObj->getWorkflowInstance())) { + throw new Exception( + _t( + 'FrontEndWorkflowController.FRONTENDACTION_TRANSITION_EXCEPTION', + 'You do not have permission to execute this action' + ) + ); + } + + //Only Save data when Transition is 'Active' + if ($this->getCurrentTransition()->Type == 'Active') { + //Hand off to WorkflowAction to perform Save + $svc = singleton(WorkflowService::class); + $active = $svc->getWorkflowFor($obj); + + $active->doFrontEndAction($data, $form, $request); + } + + //run execute on WorkflowInstance instance + $action = $this->contextObj->getWorkflowInstance()->currentAction(); + $action->BaseAction()->execute($this->contextObj->getWorkflowInstance()); + + //get valid transitions + $transitions = $action->getValidTransitions(); + + //tell instance to execute transition if it's in the permitted list + if ($transitions->find('ID', $this->transitionID)) { + $this->contextObj->getWorkflowInstance()->performTransition($this->getCurrentTransition()); + } + } + + /** + * checks to see if there is a title set on the current workflow action + * uses that or falls back to controller->Title + */ + public function Title() + { + if (!$this->Title) { + if ($this->getContextObject()) { + if ($workflow = $this->contextObj->getWorkflowInstance()) { + $this->Title = $workflow->currentAction()->BaseAction()->PageTitle ? $workflow->currentAction()->BaseAction()->PageTitle : $workflow->currentAction()->Title; + } + } + } + return $this->Title; + } +} diff --git a/code/dataobjects/ImportedWorkflowTemplate.php b/code/dataobjects/ImportedWorkflowTemplate.php index 24d3945b..68691711 100644 --- a/code/dataobjects/ImportedWorkflowTemplate.php +++ b/code/dataobjects/ImportedWorkflowTemplate.php @@ -1,6 +1,9 @@ "Varchar(255)", - "Filename" => "Varchar(255)", - "Content" => "Text" - ); +class ImportedWorkflowTemplate extends DataObject +{ + /** + * + * @var array + */ + private static $db = array( + "Name" => "Varchar(255)", + "Filename" => "Varchar(255)", + "Content" => "Text" + ); - /** - * - * @var array - */ - private static $has_one = array( - 'Definition' => 'WorkflowDefinition' - ); + /** + * + * @var array + */ + private static $has_one = array( + 'Definition' => WorkflowDefinition::class + ); + private static $table_name = 'ImportedWorkflowTemplate'; } diff --git a/code/dataobjects/WorkflowAction.php b/code/dataobjects/WorkflowAction.php index 30544648..91af8c4c 100644 --- a/code/dataobjects/WorkflowAction.php +++ b/code/dataobjects/WorkflowAction.php @@ -1,7 +1,18 @@ 'Varchar(255)', - 'Comment' => 'Text', - 'Type' => "Enum('Dynamic,Manual','Manual')", // is this used? - 'Executed' => 'Boolean', - 'AllowEditing' => "Enum('By Assignees,Content Settings,No','No')", // can this item be edited? - 'Sort' => 'Int', - 'AllowCommenting' => 'Boolean' - ); - - private static $defaults = array( - 'AllowCommenting' => '1', - ); - - private static $default_sort = 'Sort'; - - private static $has_one = array( - 'WorkflowDef' => 'WorkflowDefinition', - 'Member' => 'SilverStripe\\Security\\Member' - ); - - private static $has_many = array( - 'Transitions' => 'WorkflowTransition.Action' - ); - - /** - * The type of class to use for instances of this workflow action that are used for storing the - * data of the instance. - * - * @var string - */ - private static $instance_class = 'WorkflowActionInstance'; +class WorkflowAction extends DataObject +{ + private static $db = array( + 'Title' => 'Varchar(255)', + 'Comment' => 'Text', + 'Type' => "Enum('Dynamic,Manual','Manual')", // is this used? + 'Executed' => 'Boolean', + 'AllowEditing' => "Enum('By Assignees,Content Settings,No','No')", // can this item be edited? + 'Sort' => 'Int', + 'AllowCommenting' => 'Boolean' + ); + + private static $defaults = array( + 'AllowCommenting' => '1', + ); + + private static $default_sort = 'Sort'; + + private static $has_one = array( + 'WorkflowDef' => WorkflowDefinition::class, + 'Member' => Member::class + ); + + private static $has_many = array( + 'Transitions' => WorkflowTransition::class . '.Action' + ); - private static $icon = 'advancedworkflow/images/action.png'; + /** + * The type of class to use for instances of this workflow action that are used for storing the + * data of the instance. + * + * @var string + */ + private static $instance_class = WorkflowActionInstance::class; - /** - * Can documents in the current workflow state be edited? - * - * Only return true or false if this is an absolute value; the WorkflowActionInstance - * will try and figure out an appropriate value for the actively running workflow - * if null is returned from this method. - * - * @param DataObject $target - * @return bool - */ - public function canEditTarget(DataObject $target) { - return null; - } + private static $icon = 'advancedworkflow/images/action.png'; - /** - * Does this action restrict viewing of the document? - * - * @param DataObject $target - * @return bool - */ - public function canViewTarget(DataObject $target) { - return null; - } + private static $table_name = 'WorkflowAction'; - /** - * Does this action restrict the publishing of a document? - * - * @param DataObject $target - * @return bool - */ - public function canPublishTarget(DataObject $target) { - return null; - } + /** + * Can documents in the current workflow state be edited? + * + * Only return true or false if this is an absolute value; the WorkflowActionInstance + * will try and figure out an appropriate value for the actively running workflow + * if null is returned from this method. + * + * @param DataObject $target + * @return bool + */ + public function canEditTarget(DataObject $target) + { + return null; + } + + /** + * Does this action restrict viewing of the document? + * + * @param DataObject $target + * @return bool + */ + public function canViewTarget(DataObject $target) + { + return null; + } + + /** + * Does this action restrict the publishing of a document? + * + * @param DataObject $target + * @return bool + */ + public function canPublishTarget(DataObject $target) + { + return null; + } /** * Allows users who have permission to create a WorkflowDefinition, to create actions on it too. @@ -90,171 +106,188 @@ public function canPublishTarget(DataObject $target) { * @param array $context * @return bool */ - public function canCreate($member = null, $context = array()) { - return $this->WorkflowDef()->canCreate($member, $context); - } + public function canCreate($member = null, $context = array()) + { + return $this->WorkflowDef()->canCreate($member, $context); + } - /** - * @param Member $member - * @return bool - */ - public function canEdit($member = null) { - return $this->canCreate($member); - } + /** + * @param Member $member + * @return bool + */ + public function canEdit($member = null) + { + return $this->canCreate($member); + } - /** - * @param Member $member - * @return bool - */ - public function canDelete($member = null) { - return $this->WorkflowDef()->canDelete($member); - } + /** + * @param Member $member + * @return bool + */ + public function canDelete($member = null) + { + return $this->WorkflowDef()->canDelete($member); + } - /* + /* * If there is only a single action defined for a workflow, there's no sense * in allowing users to add a transition to it (and causing errors). * Hide the "Add Transition" button in this case * * @return boolean true if we should disable the button, false otherwise */ - public function canAddTransition() { - return ($this->WorkflowDef()->numChildren() >1); - } - - /** - * Gets an object that is used for saving the actual state of things during - * a running workflow. It still uses the workflow action def for managing the - * functional execution, however if you need to store additional data for - * the state, you can specify your own WorkflowActionInstance instead of - * the default to capture these elements - * - * @return WorkflowActionInstance - */ - public function getInstanceForWorkflow() { - $instanceClass = $this->stat('instance_class'); - $instance = new $instanceClass(); - $instance->BaseActionID = $this->ID; - return $instance; - } - - /** - * Perform whatever needs to be done for this action. If this action can be considered executed, then - * return true - if not (ie it needs some user input first), return false and 'execute' will be triggered - * again at a later point in time after the user has provided more data, either directly or indirectly. - * - * @param WorkflowInstance $workflow - * @return bool Returns true if this action has finished. - */ - public function execute(WorkflowInstance $workflow) { - return true; - } - - public function onBeforeWrite() { - if(!$this->Sort) { - $this->Sort = DB::query('SELECT MAX("Sort") + 1 FROM "WorkflowAction"')->value(); - } - - parent::onBeforeWrite(); - } - - /** - * When deleting an action from a workflow definition, make sure that workflows currently paused on that action - * are deleted - * Also removes all outbound transitions - */ - public function onAfterDelete() { - parent::onAfterDelete(); - $wfActionInstances = WorkflowActionInstance::get() - ->leftJoin("WorkflowInstance",'"WorkflowInstance"."ID" = "WorkflowActionInstance"."WorkflowID"') - ->where(sprintf('"BaseActionID" = %d AND ("WorkflowStatus" IN (\'Active\',\'Paused\'))', $this->ID)); - foreach ($wfActionInstances as $wfActionInstance){ - $wfInstances = WorkflowInstance::get()->filter('CurrentActionID', $wfActionInstance->ID); - foreach ($wfInstances as $wfInstance){ - $wfInstance->Groups()->removeAll(); - $wfInstance->Users()->removeAll(); - $wfInstance->delete(); - } - $wfActionInstance->delete(); - } - // Delete outbound transitions - $transitions = WorkflowTransition::get()->filter('ActionID', $this->ID); - foreach ($transitions as $transition){ - $transition->Groups()->removeAll(); - $transition->Users()->removeAll(); - $transition->delete(); - } - } - - /** - * Called when the current target of the workflow has been updated - */ - public function targetUpdated(WorkflowInstance $workflow) { - } - - /* CMS RELATED FUNCTIONALITY... */ - - - public function numChildren() { - return count($this->Transitions()); - } - - public function getCMSFields() { - - $fields = new FieldList(new TabSet('Root')); - $typeLabel = _t('WorkflowAction.CLASS_LABEL', 'Action Class'); - $fields->addFieldToTab('Root.Main', new ReadOnlyField('WorkflowActionClass', $typeLabel, $this->singular_name())); - $titleField = new TextField('Title', $this->fieldLabel('Title')); - $titleField->setDescription(_t( - 'WorkflowAction.TitleDescription', - 'The Title is used as the button label for this Workflow Action' - )); - $fields->addFieldToTab('Root.Main', $titleField); - $fields->addFieldToTab('Root.Main', new DropdownField('AllowEditing', $this->fieldLabel('AllowEditing'), - array( - 'By Assignees' => _t('AllowEditing.ByAssignees', 'By Assignees'), - 'Content Settings' => _t('AllowEditing.ContentSettings', 'Content Settings'), - 'No' => _t('AllowEditing.NoString', 'No') - ), - _t('AllowEditing.NoString', 'No') - )); - $fields->addFieldToTab('Root.Main', new CheckboxField('AllowCommenting', $this->fieldLabel('AllowCommenting'),$this->AllowCommenting)); - $this->extend('updateCMSFields', $fields); - return $fields; - } - - public function getValidator() { - return new RequiredFields('Title'); - } - - public function summaryFields() { - return array( - 'Title' => $this->fieldLabel('Title'), - 'Transitions' => $this->fieldLabel('Transitions'), - ); - } - - public function fieldLabels($includerelations = true) { - $labels = parent::fieldLabels($includerelations); - $labels['Comment'] = _t('WorkflowAction.CommentLabel', 'Comment'); - $labels['Type'] = _t('WorkflowAction.TypeLabel', 'Type'); - $labels['Executed'] = _t('WorkflowAction.ExecutedLabel', 'Executed'); - $labels['AllowEditing'] = _t('WorkflowAction.ALLOW_EDITING', 'Allow editing during this step?'); - $labels['Title'] = _t('WorkflowAction.TITLE', 'Title'); - $labels['AllowCommenting'] = _t('WorkflowAction.ALLOW_COMMENTING','Allow Commenting?'); - $labels['Transitions'] = _t('WorkflowAction.Transitions','Transitions'); - - return $labels; - } - - /** - * Used for Front End Workflows - */ - public function updateFrontendWorkflowFields($fields, $workflow){ + public function canAddTransition() + { + return ($this->WorkflowDef()->numChildren() >1); + } - } + /** + * Gets an object that is used for saving the actual state of things during + * a running workflow. It still uses the workflow action def for managing the + * functional execution, however if you need to store additional data for + * the state, you can specify your own WorkflowActionInstance instead of + * the default to capture these elements + * + * @return WorkflowActionInstance + */ + public function getInstanceForWorkflow() + { + $instanceClass = $this->config()->get('instance_class'); + $instance = new $instanceClass(); + $instance->BaseActionID = $this->ID; + return $instance; + } + + /** + * Perform whatever needs to be done for this action. If this action can be considered executed, then + * return true - if not (ie it needs some user input first), return false and 'execute' will be triggered + * again at a later point in time after the user has provided more data, either directly or indirectly. + * + * @param WorkflowInstance $workflow + * @return bool Returns true if this action has finished. + */ + public function execute(WorkflowInstance $workflow) + { + return true; + } + + public function onBeforeWrite() + { + if (!$this->Sort) { + $this->Sort = DB::query('SELECT MAX("Sort") + 1 FROM "WorkflowAction"')->value(); + } + + parent::onBeforeWrite(); + } + /** + * When deleting an action from a workflow definition, make sure that workflows currently paused on that action + * are deleted + * Also removes all outbound transitions + */ + public function onAfterDelete() + { + parent::onAfterDelete(); + $wfActionInstances = WorkflowActionInstance::get() + /** @skipUpgrade */ + ->leftJoin('WorkflowInstance', '"WorkflowInstance"."ID" = "WorkflowActionInstance"."WorkflowID"') + ->where(sprintf('"BaseActionID" = %d AND ("WorkflowStatus" IN (\'Active\',\'Paused\'))', $this->ID)); + foreach ($wfActionInstances as $wfActionInstance) { + $wfInstances = WorkflowInstance::get()->filter('CurrentActionID', $wfActionInstance->ID); + foreach ($wfInstances as $wfInstance) { + $wfInstance->Groups()->removeAll(); + $wfInstance->Users()->removeAll(); + $wfInstance->delete(); + } + $wfActionInstance->delete(); + } + // Delete outbound transitions + $transitions = WorkflowTransition::get()->filter('ActionID', $this->ID); + foreach ($transitions as $transition) { + $transition->Groups()->removeAll(); + $transition->Users()->removeAll(); + $transition->delete(); + } + } - public function Icon() { - return $this->stat('icon'); - } + /** + * Called when the current target of the workflow has been updated + */ + public function targetUpdated(WorkflowInstance $workflow) + { + } + + /* CMS RELATED FUNCTIONALITY... */ + + + public function numChildren() + { + return $this->Transitions()->count(); + } + + public function getCMSFields() + { + + $fields = new FieldList(new TabSet('Root')); + $typeLabel = _t('WorkflowAction.CLASS_LABEL', 'Action Class'); + $fields->addFieldToTab('Root.Main', new ReadOnlyField('WorkflowActionClass', $typeLabel, $this->singular_name())); + $titleField = new TextField('Title', $this->fieldLabel('Title')); + $titleField->setDescription(_t( + 'WorkflowAction.TitleDescription', + 'The Title is used as the button label for this Workflow Action' + )); + $fields->addFieldToTab('Root.Main', $titleField); + $fields->addFieldToTab('Root.Main', new DropdownField( + 'AllowEditing', + $this->fieldLabel('AllowEditing'), + array( + 'By Assignees' => _t('AllowEditing.ByAssignees', 'By Assignees'), + 'Content Settings' => _t('AllowEditing.ContentSettings', 'Content Settings'), + 'No' => _t('AllowEditing.NoString', 'No') + ), + _t('AllowEditing.NoString', 'No') + )); + $fields->addFieldToTab('Root.Main', new CheckboxField('AllowCommenting', $this->fieldLabel('AllowCommenting'), $this->AllowCommenting)); + $this->extend('updateCMSFields', $fields); + return $fields; + } + + public function getValidator() + { + return new RequiredFields('Title'); + } + + public function summaryFields() + { + return array( + 'Title' => $this->fieldLabel('Title'), + 'Transitions' => $this->fieldLabel('Transitions'), + ); + } + + public function fieldLabels($includerelations = true) + { + $labels = parent::fieldLabels($includerelations); + $labels['Comment'] = _t('WorkflowAction.CommentLabel', 'Comment'); + $labels['Type'] = _t('WorkflowAction.TypeLabel', 'Type'); + $labels['Executed'] = _t('WorkflowAction.ExecutedLabel', 'Executed'); + $labels['AllowEditing'] = _t('WorkflowAction.ALLOW_EDITING', 'Allow editing during this step?'); + $labels['Title'] = _t('WorkflowAction.TITLE', 'Title'); + $labels['AllowCommenting'] = _t('WorkflowAction.ALLOW_COMMENTING', 'Allow Commenting?'); + $labels['Transitions'] = _t('WorkflowAction.Transitions', 'Transitions'); + + return $labels; + } + + /** + * Used for Front End Workflows + */ + public function updateFrontendWorkflowFields($fields, $workflow) + { + } + + public function Icon() + { + return $this->config()->get('icon'); + } } diff --git a/code/dataobjects/WorkflowActionInstance.php b/code/dataobjects/WorkflowActionInstance.php index 61568d4d..ecf3fb42 100644 --- a/code/dataobjects/WorkflowActionInstance.php +++ b/code/dataobjects/WorkflowActionInstance.php @@ -1,5 +1,11 @@ 'Text', - 'Finished' => 'Boolean' - ); - - private static $has_one = array( - 'Workflow' => 'WorkflowInstance', - 'BaseAction' => 'WorkflowAction', - 'Member' => 'SilverStripe\\Security\\Member' - ); - - private static $summary_fields = array( - 'BaseAction.Title', - 'Comment', - 'Created', - 'Member.Name', - ); - - public function fieldLabels($includerelations = true) { - $labels = parent::fieldLabels($includerelations); - $labels['BaseAction.Title'] = _t('WorkflowActionInstance.Title', 'Title'); - $labels['Comment'] = _t('WorkflowAction.CommentLabel', 'Comment'); - $labels['Member.Name'] = _t('WorkflowAction.Author', 'Author'); - $labels['Finished'] = _t('WorkflowAction.FinishedLabel', 'Finished'); - $labels['BaseAction.Title'] = _t('WorkflowAction.TITLE', 'Title'); - - return $labels; - } - - /** - * Gets fields for when this is part of an active workflow - */ - public function updateWorkflowFields($fields) { +class WorkflowActionInstance extends DataObject +{ + private static $db = array( + 'Comment' => 'Text', + 'Finished' => 'Boolean' + ); + + private static $has_one = array( + 'Workflow' => WorkflowInstance::class, + 'BaseAction' => WorkflowAction::class, + 'Member' => Member::class, + ); + + private static $summary_fields = array( + 'BaseAction.Title', + 'Comment', + 'Created', + 'Member.Name', + ); + + private static $table_name = 'WorkflowActionInstance'; + + public function fieldLabels($includerelations = true) + { + $labels = parent::fieldLabels($includerelations); + $labels['BaseAction.Title'] = _t('WorkflowActionInstance.Title', 'Title'); + $labels['Comment'] = _t('WorkflowAction.CommentLabel', 'Comment'); + $labels['Member.Name'] = _t('WorkflowAction.Author', 'Author'); + $labels['Finished'] = _t('WorkflowAction.FinishedLabel', 'Finished'); + $labels['BaseAction.Title'] = _t('WorkflowAction.TITLE', 'Title'); + + return $labels; + } + + /** + * Gets fields for when this is part of an active workflow + */ + public function updateWorkflowFields($fields) + { $fieldDiff = $this->Workflow()->getTargetDiff(); - foreach($fieldDiff as $field) { + foreach ($fieldDiff as $field) { $display = ReadonlyField::create('workflow-' . $field->Name, $field->Title, $field->Diff) ->setDontEscape(true) ->addExtraClass('workflow-field-diff'); $fields->push($display); } - if ($this->BaseAction()->AllowCommenting) { - $fields->push(new TextareaField('Comment', _t('WorkflowAction.COMMENT', 'Comment'))); - } - } - - public function updateFrontendWorkflowFields($fields){ - if ($this->BaseAction()->AllowCommenting) { - $fields->push(new TextareaField('WorkflowActionInstanceComment', _t('WorkflowAction.FRONTENDCOMMENT', 'Comment'))); - } - - $ba = $this->BaseAction(); - $fields = $ba->updateFrontendWorkflowFields($fields, $this->Workflow()); - } - - /** - * Gets Front-End DataObject - * - * Use the DataObject as defined in the WorkflowAction, otherwise fall back to the - * context object. - * - * Useful for situations where front end workflow deals with multiple data objects - * - * @return DataObject - */ - public function getFrontEndDataObject() { - $obj = null; - $ba = $this->BaseAction(); - - if ($ba->hasMethod('getFrontEndDataObject')) { - $obj = $ba->getFrontEndDataObject(); - } else { - $obj = $this->Workflow()->getTarget(); - } - - return $obj; - } - - public function updateFrontEndWorkflowActions($actions) { - $ba = $this->BaseAction(); - - if ($ba->hasMethod('updateFrontEndWorkflowActions')) { - $ba->updateFrontEndWorkflowActions($actions); - } - } - - public function getRequiredFields() { - $validator = null; - $ba = $this->BaseAction(); - - if ($ba->hasMethod('getRequiredFields')) { - $validator = $ba->getRequiredFields(); - } - - return $validator; - } - - public function setFrontendFormRequirements() { - $ba = $this->BaseAction(); - - if ($ba->hasMethod('setFrontendFormRequirements')) { - $ba->setFrontendFormRequirements(); - } - } - - public function doFrontEndAction(array $data, Form $form, SS_HTTPRequest $request) { - //Save Front End Workflow notes, then hand over to Workflow Action - if (isset($data["WorkflowActionInstanceComment"])) { - $this->Comment = $data["WorkflowActionInstanceComment"]; - $this->write(); - } - - $ba = $this->BaseAction(); - if ($ba->hasMethod('doFrontEndAction')) { - $ba->doFrontEndAction($data, $form, $request); - } - } - - - /** - * Gets the title of this active action instance - * - * @return string - */ - public function getTitle() { - return $this->BaseAction()->Title; - } - - /** - * Returns all the valid transitions that lead out from this action. - * - * This is called if this action has finished, and the workflow engine wants - * to run the next action. - * - * If this action returns only one valid transition it will be immediately - * followed; otherwise the user will decide which transition to follow. - * - * @return ArrayList - */ - public function getValidTransitions() { - $available = $this->BaseAction()->Transitions(); - $valid = new ArrayList(); - - // iterate through the transitions and see if they're valid for the current state of the item being - // workflowed - if($available) foreach($available as $transition) { - if($transition->isValid($this->Workflow())) $valid->push($transition); - } - + if ($this->BaseAction()->AllowCommenting) { + $fields->push(new TextareaField('Comment', _t('WorkflowAction.COMMENT', 'Comment'))); + } + } + + public function updateFrontendWorkflowFields($fields) + { + if ($this->BaseAction()->AllowCommenting) { + $fields->push(new TextareaField('WorkflowActionInstanceComment', _t('WorkflowAction.FRONTENDCOMMENT', 'Comment'))); + } + + $ba = $this->BaseAction(); + $fields = $ba->updateFrontendWorkflowFields($fields, $this->Workflow()); + } + + /** + * Gets Front-End DataObject + * + * Use the DataObject as defined in the WorkflowAction, otherwise fall back to the + * context object. + * + * Useful for situations where front end workflow deals with multiple data objects + * + * @return DataObject + */ + public function getFrontEndDataObject() + { + $obj = null; + $ba = $this->BaseAction(); + + if ($ba->hasMethod('getFrontEndDataObject')) { + $obj = $ba->getFrontEndDataObject(); + } else { + $obj = $this->Workflow()->getTarget(); + } + + return $obj; + } + + public function updateFrontEndWorkflowActions($actions) + { + $ba = $this->BaseAction(); + + if ($ba->hasMethod('updateFrontEndWorkflowActions')) { + $ba->updateFrontEndWorkflowActions($actions); + } + } + + public function getRequiredFields() + { + $validator = null; + $ba = $this->BaseAction(); + + if ($ba->hasMethod('getRequiredFields')) { + $validator = $ba->getRequiredFields(); + } + + return $validator; + } + + public function setFrontendFormRequirements() + { + $ba = $this->BaseAction(); + + if ($ba->hasMethod('setFrontendFormRequirements')) { + $ba->setFrontendFormRequirements(); + } + } + + public function doFrontEndAction(array $data, Form $form, HTTPRequest $request) + { + //Save Front End Workflow notes, then hand over to Workflow Action + if (isset($data["WorkflowActionInstanceComment"])) { + $this->Comment = $data["WorkflowActionInstanceComment"]; + $this->write(); + } + + $ba = $this->BaseAction(); + if ($ba->hasMethod('doFrontEndAction')) { + $ba->doFrontEndAction($data, $form, $request); + } + } + + + /** + * Gets the title of this active action instance + * + * @return string + */ + public function getTitle() + { + return $this->BaseAction()->Title; + } + + /** + * Returns all the valid transitions that lead out from this action. + * + * This is called if this action has finished, and the workflow engine wants + * to run the next action. + * + * If this action returns only one valid transition it will be immediately + * followed; otherwise the user will decide which transition to follow. + * + * @return ArrayList + */ + public function getValidTransitions() + { + $available = $this->BaseAction()->Transitions(); + $valid = new ArrayList(); + + // iterate through the transitions and see if they're valid for the current state of the item being + // workflowed + if ($available) { + foreach ($available as $transition) { + if ($transition->isValid($this->Workflow())) { + $valid->push($transition); + } + } + } + $this->extend('updateValidTransitions', $valid); - return $valid; - } - - /** - * Called when this instance is started within the workflow - */ - public function actionStart(WorkflowTransition $transition) { - $this->extend('onActionStart', $transition); - } - - /** - * Called when this action has been completed within the workflow - */ - public function actionComplete(WorkflowTransition $transition) { - $this->MemberID = Member::currentUserID(); - $this->write(); - $this->extend('onActionComplete', $transition); - } - - - /** - * Can documents in the current workflow state be edited? - * - * @param DataObject $target - * @return bool - */ - public function canEditTarget(DataObject $target) { - $absolute = $this->BaseAction()->canEditTarget($target); - if (!is_null($absolute)) { - return $absolute; - } - switch ($this->BaseAction()->AllowEditing) { - case 'By Assignees': - return $this->Workflow()->canEdit(); - case 'No': - return false; - case 'Content Settings': - default: - return null; - } - } - - /** - * Does this action restrict viewing of the document? - * - * @param DataObject $target - * @return bool - */ - public function canViewTarget(DataObject $target) { - return $this->BaseAction()->canViewTarget($target); - } - - /** - * Does this action restrict the publishing of a document? - * - * @param DataObject $target - * @return bool - */ - public function canPublishTarget(DataObject $target) { - $absolute = $this->BaseAction()->canPublishTarget($target); - if (!is_null($absolute)) { - return $absolute; - } - return false; - } - - public function canView($member = null) { - return $this->Workflow()->canView($member); - } - - public function canEdit($member = null) { - return $this->Workflow()->canEdit($member); - } - - public function canDelete($member = null) { - return $this->Workflow()->canDelete($member); - } + return $valid; + } + + /** + * Called when this instance is started within the workflow + */ + public function actionStart(WorkflowTransition $transition) + { + $this->extend('onActionStart', $transition); + } + + /** + * Called when this action has been completed within the workflow + */ + public function actionComplete(WorkflowTransition $transition) + { + $this->MemberID = Member::currentUserID(); + $this->write(); + $this->extend('onActionComplete', $transition); + } + + + /** + * Can documents in the current workflow state be edited? + * + * @param DataObject $target + * @return bool + */ + public function canEditTarget(DataObject $target) + { + $absolute = $this->BaseAction()->canEditTarget($target); + if (!is_null($absolute)) { + return $absolute; + } + switch ($this->BaseAction()->AllowEditing) { + case 'By Assignees': + return $this->Workflow()->canEdit(); + case 'No': + return false; + case 'Content Settings': + default: + return null; + } + } + + /** + * Does this action restrict viewing of the document? + * + * @param DataObject $target + * @return bool + */ + public function canViewTarget(DataObject $target) + { + return $this->BaseAction()->canViewTarget($target); + } + + /** + * Does this action restrict the publishing of a document? + * + * @param DataObject $target + * @return bool + */ + public function canPublishTarget(DataObject $target) + { + $absolute = $this->BaseAction()->canPublishTarget($target); + if (!is_null($absolute)) { + return $absolute; + } + return false; + } + + public function canView($member = null) + { + return $this->Workflow()->canView($member); + } + + public function canEdit($member = null) + { + return $this->Workflow()->canEdit($member); + } + + public function canDelete($member = null) + { + return $this->Workflow()->canDelete($member); + } } diff --git a/code/dataobjects/WorkflowDefinition.php b/code/dataobjects/WorkflowDefinition.php index bbca51ab..8de848b4 100644 --- a/code/dataobjects/WorkflowDefinition.php +++ b/code/dataobjects/WorkflowDefinition.php @@ -1,9 +1,37 @@ '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')); - } - - /* +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::class, + 'Instances' => WorkflowInstance::class + ); + + /** + * 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' => Member::class, + 'Groups' => Group::class, + ); + + 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::class, + ); + + private static $table_name = 'WorkflowDefinition'; + + /** + * @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::class, $action->ID)) { + $orphan->delete(); + } + }); + } + + /** + * + * Deletes related ImportedWorkflowTemplate objects. + * + * @return void + */ + private function deleteRelatedImport() + { + if ($import = DataObject::get(ImportedWorkflowTemplate::class)->filter('DefinitionID', $this->ID)->first()) { + $import->delete(); + } + } + + /** + * @return int + */ + public function numChildren() + { + return $this->Actions()->count(); + } + + 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'), Group::class)); + } + + 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(); @@ -244,142 +286,146 @@ public function getCMSFields() { * } */ - $message = _t( - 'WorkflowDefinition.ADDAFTERSAVING', - 'You can add workflow steps after you save for the first time.' - ); - $fields->addFieldToTab('Root.Main', new LiteralField( - 'AddAfterSaving', "

$message

" - )); - } - - 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; - } + $message = _t( + 'WorkflowDefinition.ADDAFTERSAVING', + 'You can add workflow steps after you save for the first time.' + ); + $fields->addFieldToTab('Root.Main', new LiteralField( + 'AddAfterSaving', + "

$message

" + )); + } + + 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::class); + $active->getConfig()->removeComponentsByType(GridFieldDeleteAction::class); + + if (!Permission::check('REASSIGN_ACTIVE_WORKFLOWS')) { + $active->getConfig()->removeComponentsByType(GridFieldEditButton::class); + $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 = WorkflowDefinition::get()->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 = ImportedWorkflowTemplate::get()->filter('Filename', $req['_CsvFile']['name'])->first(); + $incomingTitle = $import->Name; + } elseif (isset($req['Template']) && !empty($req['Template'])) { + $incomingTitle = $req['Template']; + } elseif (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 @@ -407,59 +453,63 @@ public function canWorkflowPublish($member, $target) * @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; - } - - /* + public function canCreate($member = null, $context = array()) + { + if (is_null($member)) { + if (!Security::getCurrentUser()) { + return false; + } + $member = Security::getCurrentUser(); + } + 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 (!Security::getCurrentUser()) { + return false; + } + $member = Security::getCurrentUser(); + } + + 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'); - } + return Permission::checkMember($member, 'DELETE_WORKFLOW'); + } /** * Checks whether the passed user is able to view this ModelAdmin @@ -467,16 +517,17 @@ public function canDelete($member = null) { * @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; - } - } + protected function userHasAccess($member) + { + if (!$member) { + if (!Security::getCurrentUser()) { + return false; + } + $member = Security::getCurrentUser(); + } + + if (Permission::checkMember($member, "VIEW_ACTIVE_WORKFLOWS")) { + return true; + } + } } diff --git a/code/dataobjects/WorkflowInstance.php b/code/dataobjects/WorkflowInstance.php index 3eca065c..8d593c8a 100644 --- a/code/dataobjects/WorkflowInstance.php +++ b/code/dataobjects/WorkflowInstance.php @@ -1,12 +1,35 @@ 'Varchar(128)', - 'WorkflowStatus' => "Enum('Active,Paused,Complete,Cancelled','Active')", - 'TargetClass' => 'Varchar(64)', - 'TargetID' => 'Int', - ); - - private static $has_one = array( - 'Definition' => 'WorkflowDefinition', - 'CurrentAction' => 'WorkflowActionInstance', - 'Initiator' => 'SilverStripe\\Security\\Member', - ); - - private static $has_many = array( - 'Actions' => 'WorkflowActionInstance', - ); - - /** - * The list of users who are responsible for performing the current WorkflowAction - * - * @var array - */ - private static $many_many = array( - 'Users' => 'SilverStripe\\Security\\Member', - 'Groups' => 'SilverStripe\\Security\\Group' - ); - - private static $summary_fields = array( - 'Title', - 'WorkflowStatus', - 'Created' - ); +class WorkflowInstance extends DataObject +{ + private static $db = array( + 'Title' => 'Varchar(128)', + 'WorkflowStatus' => "Enum('Active,Paused,Complete,Cancelled','Active')", + 'TargetClass' => 'Varchar(255)', + 'TargetID' => 'Int', + ); + + private static $has_one = array( + 'Definition' => WorkflowDefinition::class, + 'CurrentAction' => WorkflowActionInstance::class, + 'Initiator' => Member::class, + ); + + private static $has_many = array( + 'Actions' => WorkflowActionInstance::class, + ); + + /** + * The list of users who are responsible for performing the current WorkflowAction + * + * @var array + */ + private static $many_many = array( + 'Users' => Member::class, + 'Groups' => Group::class, + ); + + private static $summary_fields = array( + 'Title', + 'WorkflowStatus', + 'Created' + ); private static $default_sort = array( '"Created"' => 'DESC' ); - /** - * If set to true, actions that cannot be executed by the user will not show - * on the frontend (just like the backend). - * - * @var boolean - */ - private static $hide_disabled_actions_on_frontend = false; + /** + * If set to true, actions that cannot be executed by the user will not show + * on the frontend (just like the backend). + * + * @var boolean + */ + private static $hide_disabled_actions_on_frontend = false; /** * Fields to ignore when generating a diff for data objects. @@ -83,666 +106,700 @@ class WorkflowInstance extends DataObject { 'UnPublishJobID' ); - /** - * Get the CMS view of the instance. This is used to display the log of - * this workflow, and options to reassign if the workflow hasn't been - * finished yet - * - * @return \FieldList - */ - public function getCMSFields() { - $fields = new FieldList(); + private static $table_name = 'WorkflowInstance'; + + /** + * Get the CMS view of the instance. This is used to display the log of + * this workflow, and options to reassign if the workflow hasn't been + * finished yet + * + * @return FieldList + */ + public function getCMSFields() + { + $fields = new FieldList(); $fields->push(new TabSet('Root', new Tab('Main'))); - if (Permission::check('REASSIGN_ACTIVE_WORKFLOWS')) { - if ($this->WorkflowStatus == 'Paused' || $this->WorkflowStatus == 'Active') { - $cmsUsers = Member::mapInCMSGroups(); + if (Permission::check('REASSIGN_ACTIVE_WORKFLOWS')) { + if ($this->WorkflowStatus == 'Paused' || $this->WorkflowStatus == 'Active') { + $cmsUsers = Member::mapInCMSGroups(); $fields->addFieldsToTab('Root.Main', array( new HiddenField('DirectUpdate', '', 1), - new HeaderField('InstanceReassignHeader',_t('WorkflowInstance.REASSIGN_HEADER', 'Reassign workflow')), + new HeaderField('InstanceReassignHeader', _t('WorkflowInstance.REASSIGN_HEADER', 'Reassign workflow')), new CheckboxSetField('Users', _t('WorkflowDefinition.USERS', 'Users'), $cmsUsers), - new TreeMultiselectField('Groups', _t('WorkflowDefinition.GROUPS', 'Groups'), 'SilverStripe\\Security\\Group') + new TreeMultiselectField('Groups', _t('WorkflowDefinition.GROUPS', 'Groups'), Group::class) )); + } + } + + if ($this->canEdit()) { + $action = $this->CurrentAction(); + if ($action->exists()) { + $actionFields = $this->getWorkflowFields(); + $fields->addFieldsToTab('Root.Main', $actionFields); + + $transitions = $action->getValidTransitions(); + if ($transitions) { + $fields->replaceField('TransitionID', DropdownField::create("TransitionID", "Next action", $transitions->map())); + } + } + } + + $items = WorkflowActionInstance::get()->filter(array( + 'Finished' => 1, + 'WorkflowID' => $this->ID + )); + + $grid = new GridField( + 'Actions', + _t('WorkflowInstance.ActionLogTitle', 'Log'), + $items + ); + + $fields->addFieldsToTab('Root.Main', $grid); - } - } - - if ($this->canEdit()) { - $action = $this->CurrentAction(); - if ($action->exists()) { - $actionFields = $this->getWorkflowFields(); - $fields->addFieldsToTab('Root.Main', $actionFields); - - $transitions = $action->getValidTransitions(); - if ($transitions) { - $fields->replaceField('TransitionID', DropdownField::create("TransitionID", "Next action", $transitions->map())); - } - } - } - - $items = WorkflowActionInstance::get()->filter(array( - 'Finished' => 1, - 'WorkflowID' => $this->ID - )); - - $grid = new GridField( - 'Actions', - _t('WorkflowInstance.ActionLogTitle','Log'), - $items - ); - - $fields->addFieldsToTab('Root.Main', $grid); - - return $fields; - } - - public function fieldLabels($includerelations = true) { - $labels = parent::fieldLabels($includerelations); - $labels['Title'] = _t('WorkflowInstance.TitleLabel', 'Title'); - $labels['WorkflowStatus'] = _t('WorkflowInstance.WorkflowStatusLabel', 'Workflow Status'); - $labels['TargetClass'] = _t('WorkflowInstance.TargetClassLabel', 'Target Class'); - $labels['TargetID'] = _t('WorkflowInstance.TargetIDLabel', 'Target'); - - return $labels; - } - - /** - * See if we've been saved in context of managing the workflow directly - */ - public function onBeforeWrite() { - parent::onBeforeWrite(); - - $vars = $this->record; - - if (isset($vars['DirectUpdate'])) { - // Unset now so that we don't end up in an infinite loop! - unset($this->record['DirectUpdate']); - $this->updateWorkflow($vars); - } - } - - /** - * Update the current state of the workflow - * - * Typically, this is triggered by someone modifiying the workflow instance via the modeladmin form - * side of things when administering things, such as re-assigning or manually approving a stuck workflow - * - * Note that this is VERY similar to AdvancedWorkflowExtension::updateworkflow - * but without the formy bits. These two implementations should PROBABLY - * be merged - * - * @todo refactor with AdvancedWorkflowExtension - * - * @param type $data - * @return - */ - public function updateWorkflow($data) { - $action = $this->CurrentAction(); - - if (!$this->getTarget() || !$this->getTarget()->canEditWorkflow()) { - return; - } - - $allowedFields = $this->getWorkflowFields()->saveableFields(); - unset($allowedFields['TransitionID']); - foreach ($allowedFields as $field) { - $fieldName = $field->getName(); - $action->$fieldName = $data[$fieldName]; - } - $action->write(); - - $svc = singleton('WorkflowService'); - if (isset($data['TransitionID']) && $data['TransitionID']) { - $svc->executeTransition($this->getTarget(), $data['TransitionID']); - } else { - // otherwise, just try to execute the current workflow to see if it - // can now proceed based on user input - $this->execute(); - } - } - - /** - * Get the target-object that this WorkflowInstance "points" to. - * - * Workflows are not restricted to being active on SiteTree objects, - * so we need to account for being attached to anything. - * - * Sets Versioned::set_reading_mode() to allow fetching of Draft _and_ Published - * content. - * - * @param Boolean $getLive - * @return (null | DataObject) - */ - public function getTarget($getLive = false) { - if($this->TargetID && $this->TargetClass) { - $versionable = Injector::inst()->get($this->TargetClass)->has_extension('SilverStripe\\ORM\\Versioning\\Versioned'); - - if(!$versionable && $getLive) { - return; - } - if($versionable) { - Versioned::set_stage($getLive ? Versioned::LIVE : Versioned::DRAFT); - } - - // Default - return DataObject::get_by_id($this->TargetClass, $this->TargetID); - } - } - - /** - * - * @param Boolean $getLive - * @see {@link {$this->getTarget()} - * @return (null | DataObject) - */ - public function Target($getLive = false) { - return $this->getTarget($getLive); - } + return $fields; + } + + public function fieldLabels($includerelations = true) + { + $labels = parent::fieldLabels($includerelations); + $labels['Title'] = _t('WorkflowInstance.TitleLabel', 'Title'); + $labels['WorkflowStatus'] = _t('WorkflowInstance.WorkflowStatusLabel', 'Workflow Status'); + $labels['TargetClass'] = _t('WorkflowInstance.TargetClassLabel', 'Target Class'); + $labels['TargetID'] = _t('WorkflowInstance.TargetIDLabel', 'Target'); + + return $labels; + } + + /** + * See if we've been saved in context of managing the workflow directly + */ + public function onBeforeWrite() + { + parent::onBeforeWrite(); + + $vars = $this->record; + + if (isset($vars['DirectUpdate'])) { + // Unset now so that we don't end up in an infinite loop! + unset($this->record['DirectUpdate']); + $this->updateWorkflow($vars); + } + } + + /** + * Update the current state of the workflow + * + * Typically, this is triggered by someone modifiying the workflow instance via the modeladmin form + * side of things when administering things, such as re-assigning or manually approving a stuck workflow + * + * Note that this is VERY similar to AdvancedWorkflowExtension::updateworkflow + * but without the formy bits. These two implementations should PROBABLY + * be merged + * + * @todo refactor with AdvancedWorkflowExtension + * + * @param type $data + * @return + */ + public function updateWorkflow($data) + { + $action = $this->CurrentAction(); + + if (!$this->getTarget() || !$this->getTarget()->canEditWorkflow()) { + return; + } + + $allowedFields = $this->getWorkflowFields()->saveableFields(); + unset($allowedFields['TransitionID']); + foreach ($allowedFields as $field) { + $fieldName = $field->getName(); + $action->$fieldName = $data[$fieldName]; + } + $action->write(); + + $svc = singleton(WorkflowService::class); + if (isset($data['TransitionID']) && $data['TransitionID']) { + $svc->executeTransition($this->getTarget(), $data['TransitionID']); + } else { + // otherwise, just try to execute the current workflow to see if it + // can now proceed based on user input + $this->execute(); + } + } + + /** + * Get the target-object that this WorkflowInstance "points" to. + * + * Workflows are not restricted to being active on SiteTree objects, + * so we need to account for being attached to anything. + * + * Sets Versioned::set_reading_mode() to allow fetching of Draft _and_ Published + * content. + * + * @param boolean $getLive + * @return null|DataObject + */ + public function getTarget($getLive = false) + { + if ($this->TargetID && $this->TargetClass) { + $versionable = Injector::inst()->get($this->TargetClass)->has_extension(Versioned::class); + + if (!$versionable && $getLive) { + return; + } + if ($versionable) { + Versioned::set_stage($getLive ? Versioned::LIVE : Versioned::DRAFT); + } + + // Default + return DataObject::get_by_id($this->TargetClass, $this->TargetID); + } + } + + /** + * + * @param boolean $getLive + * @see {@link {$this->getTarget()} + * @return null|DataObject + */ + public function Target($getLive = false) + { + return $this->getTarget($getLive); + } /** * Returns the field differences between the older version and current version of Target * * @return ArrayList */ - public function getTargetDiff() { + public function getTargetDiff() + { $liveTarget = $this->Target(true); $draftTarget = $this->Target(); $diff = DataDifferencer::create($liveTarget, $draftTarget); - $diff->ignoreFields($this->stat('diff_ignore_fields')); + $diff->ignoreFields($this->config()->get('diff_ignore_fields')); $fields = $diff->ChangedFields(); return $fields; } - /** - * Start a workflow based on a particular definition for a particular object. - * - * The object is optional; if not specified, it is assumed that this workflow - * is simply a task based checklist type of workflow. - * - * @param WorkflowDefinition $definition - * @param DataObject $for - */ - public function beginWorkflow(WorkflowDefinition $definition, DataObject $for=null) { - if(!$this->ID) { - $this->write(); - } - - if ($for && ($for->hasExtension('WorkflowApplicable') || $for->hasExtension('FileWorkflowApplicable'))) { - $this->TargetClass = ClassInfo::baseDataClass($for); - $this->TargetID = $for->ID; - } - - // lets create the first WorkflowActionInstance. - $action = $definition->getInitialAction()->getInstanceForWorkflow(); - $action->WorkflowID = $this->ID; - $action->write(); - - $title = $for && $for->hasField('Title') ? - sprintf(_t('WorkflowInstance.TITLE_FOR_DO', '%s - %s'), $definition->Title, $for->Title) : - sprintf(_t('WorkflowInstance.TITLE_STUB', 'Instance #%s of %s'), $this->ID, $definition->Title); - - $this->Title = $title; - $this->DefinitionID = $definition->ID; - $this->CurrentActionID = $action->ID; - $this->InitiatorID = Member::currentUserID(); - $this->write(); - - $this->Users()->addMany($definition->Users()); - $this->Groups()->addMany($definition->Groups()); - } - - /** - * Execute this workflow. In rare cases this will actually execute all actions, - * but typically, it will stop and wait for the user to input something - * - * The basic process is to get the current action, and see whether it has been finished - * by some process, if not it attempts to execute it. - * - * If it has been finished, we check to see if there's some transitions to follow. If there's - * only one transition, then we execute that immediately. - * - * If there's multiple transitions, we just stop and wait for the user to manually - * trigger a transition. - * - * If there's no transitions, we make the assumption that we've finished the workflow and - * mark it as such. - * - * - */ - public function execute() { - if (!$this->CurrentActionID) { - throw new Exception( - sprintf(_t('WorkflowInstance.EXECUTE_EXCEPTION', 'Attempted to start an invalid workflow instance #%s!'), $this->ID) - ); - } - - $action = $this->CurrentAction(); - $transition = false; - - // if the action has already finished, it means it has either multiple (or no - // transitions at the time), so a subsequent check should be run. - if($action->Finished) { - $transition = $this->checkTransitions($action); - } else { - $result = $action->BaseAction()->execute($this); - - // if the action was successful, then the action has finished running and - // next transition should be run (if only one). - // input. - if($result) { - $action->MemberID = Member::currentUserID(); - $action->Finished = true; - $action->write(); - $transition = $this->checkTransitions($action); - } - } - - // if the action finished, and there's only one available transition then - // move onto that step - otherwise check if the workflow has finished. - if($transition) { - $this->performTransition($transition); - } else { - // see if there are any transitions available, even if they are not valid. - if($action->Finished && !count($action->BaseAction()->Transitions())) { - $this->WorkflowStatus = 'Complete'; - $this->CurrentActionID = 0; - } else { - $this->WorkflowStatus = 'Paused'; - } - - $this->write(); - } - } - - /** - * Evaluate all the transitions of an action and determine whether we should - * follow any of them yet. - * - * @param WorkflowActionInstance $action - * @return WorkflowTransition - */ - protected function checkTransitions(WorkflowActionInstance $action) { - $transitions = $action->getValidTransitions(); - // if there's JUST ONE transition, then we need should - // immediately follow it. - if ($transitions && $transitions->count() == 1) { - return $transitions->First(); - } - } - - /** - * Transitions a workflow to the next step defined by the given transition. - * - * After transitioning, the action is 'executed', and next steps - * determined. - * - * @param WorkflowTransition $transition - */ - public function performTransition(WorkflowTransition $transition) { - // first make sure that the transition is valid to execute! - $action = $this->CurrentAction(); - $allTransitions = $action->BaseAction()->Transitions(); - - $valid = $allTransitions->find('ID', $transition->ID); - if (!$valid) { - throw new Exception ( - sprintf(_t('WorkflowInstance.WORKFLOW_TRANSITION_EXCEPTION', 'Invalid transition state for action #%s'), $action->ID) - ); - - } - - $action->actionComplete($transition); - - $definition = DataObject::get_by_id('WorkflowAction', $transition->NextActionID); - $action = $definition->getInstanceForWorkflow(); - $action->WorkflowID = $this->ID; - $action->write(); - - $this->CurrentActionID = $action->ID; - $this->write(); - $this->components = array(); // manually clear the has_one cache - - $action->actionStart($transition); - - $transition->extend('onTransition'); - $this->execute(); - } - - /** - * Returns a list of all Members that are assigned to this instance, either directly or via a group. - * - * @todo This could be made more efficient. - * @return ArrayList - */ - public function getAssignedMembers() { - $list = new ArrayList(); - $groups = $this->Groups(); - - $list->merge($this->Users()); - - foreach($groups as $group) { - $list->merge($group->Members()); - } - - $list->removeDuplicates(); - return $list; - } - - /** - * - * @param \Member $member - * @return boolean - */ - public function canView($member=null) { - $extended = $this->extendedCan(__FUNCTION__, $member); - if($extended !== null) return $extended; - - $hasAccess = $this->userHasAccess($member); - /* + /** + * Start a workflow based on a particular definition for a particular object. + * + * The object is optional; if not specified, it is assumed that this workflow + * is simply a task based checklist type of workflow. + * + * @param WorkflowDefinition $definition + * @param DataObject $for + */ + public function beginWorkflow(WorkflowDefinition $definition, DataObject $for = null) + { + if (!$this->ID) { + $this->write(); + } + + if ($for && ($for->hasExtension(WorkflowApplicable::class) || $for->hasExtension(FileWorkflowApplicable::class))) { + $this->TargetClass = DataObject::getSchema()->baseDataClass($for); + $this->TargetID = $for->ID; + } + + // lets create the first WorkflowActionInstance. + $action = $definition->getInitialAction()->getInstanceForWorkflow(); + $action->WorkflowID = $this->ID; + $action->write(); + + $title = $for && $for->hasField('Title') + ? sprintf(_t('WorkflowInstance.TITLE_FOR_DO', '%s - %s'), $definition->Title, $for->Title) + : sprintf(_t('WorkflowInstance.TITLE_STUB', 'Instance #%s of %s'), $this->ID, $definition->Title); + + $this->Title = $title; + $this->DefinitionID = $definition->ID; + $this->CurrentActionID = $action->ID; + $this->InitiatorID = Security::getCurrentUser()->ID; + $this->write(); + + $this->Users()->addMany($definition->Users()); + $this->Groups()->addMany($definition->Groups()); + } + + /** + * Execute this workflow. In rare cases this will actually execute all actions, + * but typically, it will stop and wait for the user to input something + * + * The basic process is to get the current action, and see whether it has been finished + * by some process, if not it attempts to execute it. + * + * If it has been finished, we check to see if there's some transitions to follow. If there's + * only one transition, then we execute that immediately. + * + * If there's multiple transitions, we just stop and wait for the user to manually + * trigger a transition. + * + * If there's no transitions, we make the assumption that we've finished the workflow and + * mark it as such. + * + * + */ + public function execute() + { + if (!$this->CurrentActionID) { + throw new Exception( + sprintf(_t('WorkflowInstance.EXECUTE_EXCEPTION', 'Attempted to start an invalid workflow instance #%s!'), $this->ID) + ); + } + + $action = $this->CurrentAction(); + $transition = false; + + // if the action has already finished, it means it has either multiple (or no + // transitions at the time), so a subsequent check should be run. + if ($action->Finished) { + $transition = $this->checkTransitions($action); + } else { + $result = $action->BaseAction()->execute($this); + + // if the action was successful, then the action has finished running and + // next transition should be run (if only one). + // input. + if ($result) { + $action->MemberID = Security::getCurrentUser()->ID; + $action->Finished = true; + $action->write(); + $transition = $this->checkTransitions($action); + } + } + + // if the action finished, and there's only one available transition then + // move onto that step - otherwise check if the workflow has finished. + if ($transition) { + $this->performTransition($transition); + } else { + // see if there are any transitions available, even if they are not valid. + if ($action->Finished && !count($action->BaseAction()->Transitions())) { + $this->WorkflowStatus = 'Complete'; + $this->CurrentActionID = 0; + } else { + $this->WorkflowStatus = 'Paused'; + } + + $this->write(); + } + } + + /** + * Evaluate all the transitions of an action and determine whether we should + * follow any of them yet. + * + * @param WorkflowActionInstance $action + * @return WorkflowTransition + */ + protected function checkTransitions(WorkflowActionInstance $action) + { + $transitions = $action->getValidTransitions(); + // if there's JUST ONE transition, then we need should + // immediately follow it. + if ($transitions && $transitions->count() == 1) { + return $transitions->First(); + } + } + + /** + * Transitions a workflow to the next step defined by the given transition. + * + * After transitioning, the action is 'executed', and next steps + * determined. + * + * @param WorkflowTransition $transition + */ + public function performTransition(WorkflowTransition $transition) + { + // first make sure that the transition is valid to execute! + $action = $this->CurrentAction(); + $allTransitions = $action->BaseAction()->Transitions(); + + $valid = $allTransitions->find('ID', $transition->ID); + if (!$valid) { + throw new Exception( + sprintf(_t('WorkflowInstance.WORKFLOW_TRANSITION_EXCEPTION', 'Invalid transition state for action #%s'), $action->ID) + ); + } + + $action->actionComplete($transition); + + $definition = DataObject::get_by_id(WorkflowAction::class, $transition->NextActionID); + $action = $definition->getInstanceForWorkflow(); + $action->WorkflowID = $this->ID; + $action->write(); + + $this->CurrentActionID = $action->ID; + $this->write(); + $this->components = array(); // manually clear the has_one cache + + $action->actionStart($transition); + + $transition->extend('onTransition'); + $this->execute(); + } + + /** + * Returns a list of all Members that are assigned to this instance, either directly or via a group. + * + * @todo This could be made more efficient. + * @return ArrayList + */ + public function getAssignedMembers() + { + $list = new ArrayList(); + $groups = $this->Groups(); + + $list->merge($this->Users()); + + foreach ($groups as $group) { + $list->merge($group->Members()); + } + + $list->removeDuplicates(); + return $list; + } + + /** + * + * @param Member $member + * @return boolean + */ + public function canView($member = null) + { + $extended = $this->extendedCan(__FUNCTION__, $member); + if ($extended !== null) { + return $extended; + } + + $hasAccess = $this->userHasAccess($member); + /* * If the next action is AssignUsersToWorkflowAction, execute() resets all user+group relations. * Therefore current user no-longer has permission to view this WorkflowInstance in PendingObjects Gridfield, even though; * - She had permissions granted via the workflow definition to run the preceeding Action that took her here. */ - if(!$hasAccess) { - if($this->getMostRecentActionForUser($member)) { - return true; - } - } - return $hasAccess; - } - - /** - * - * @param \Member $member - * @return boolean - */ - public function canEdit($member = null) { - $extended = $this->extendedCan(__FUNCTION__, $member); - if($extended !== null) return $extended; - - return $this->userHasAccess($member); - } - - /** - * - * @param \Member $member - * @return boolean - */ - public function canDelete($member = null) { - $extended = $this->extendedCan(__FUNCTION__, $member); - if($extended !== null) return $extended; - - if(Permission::checkMember($member, "DELETE_WORKFLOW")) { - return true; - } - return false; - } - - /** - * Checks whether the given user is in the list of users assigned to this - * workflow - * - * @param $memberID - */ - protected function userHasAccess($member) { - if (!$member) { - if (!Member::currentUserID()) { - return false; - } - $member = Member::currentUser(); - } - - if(Permission::checkMember($member, "ADMIN")) { - return true; - } - - // This method primarily "protects" access to a WorkflowInstance, but assumes access only to be granted to users assigned-to that WorkflowInstance. - // However; lowly authors (users entering items into a workflow) are not assigned - but we still wish them to see their submitted content. - $inWorkflowGroupOrUserTables = ($member->inGroups($this->Groups()) || $this->Users()->find('ID', $member->ID)); - // This method is used in more than just the ModelAdmin. Check for the current controller to determine where canView() expectations differ - if($this->getTarget() && Controller::curr()->getAction() == 'index' && !$inWorkflowGroupOrUserTables) { - if($this->getVersionedConnection($this->getTarget()->ID, $member->ID)) { - return true; - } - return false; - } - return $inWorkflowGroupOrUserTables; - } - - /** - * Can documents in the current workflow state be edited? - */ - public function canEditTarget() { - if ($this->CurrentActionID && ($target = $this->getTarget())) { - return $this->CurrentAction()->canEditTarget($target); - } - } - - /** - * Does this action restrict viewing of the document? - * - * @return boolean - */ - public function canViewTarget() { - $action = $this->CurrentAction(); - if ($action) { - return $action->canViewTarget($this->getTarget()); - } - return true; - } - - /** - * Does this action restrict the publishing of a document? - * - * @return boolean - */ - public function canPublishTarget() { - if ($this->CurrentActionID && ($target = $this->getTarget())) { - return $this->CurrentAction()->canPublishTarget($target); - } - } - - /** - * Get the current set of transitions that are valid for the current workflow state, - * and are available to the current user. - * - * @return array - */ - public function validTransitions() { - $action = $this->CurrentAction(); - $transitions = $action->getValidTransitions(); - - // Filter by execute permission - $self = $this; - return $transitions->filterByCallback(function($transition) use ($self) { - return $transition->canExecute($self); - }); - } - - /* UI RELATED METHODS */ - - /** - * Gets fields for managing this workflow instance in its current step - * - * @return FieldList - */ - public function getWorkflowFields() { - $action = $this->CurrentAction(); - $options = $this->validTransitions(); - $wfOptions = $options->map('ID', 'Title', ' '); - $fields = new FieldList(); - - $fields->push(new HeaderField('WorkflowHeader', $action->Title)); - - $fields->push(HiddenField::create('TransitionID', '')); - // Let the Active Action update the fields that the user can interact with so that data can be - // stored for the workflow. - $action->updateWorkflowFields($fields); - $action->invokeWithExtensions('updateWorkflowFields', $fields); - return $fields; - } - - /** - * Gets Front-End form fields from current Action - * - * @return FieldList - */ - public function getFrontEndWorkflowFields() { - $action = $this->CurrentAction(); - - $fields = new FieldList(); - $action->updateFrontEndWorkflowFields($fields); - - return $fields; - } - - /** - * Gets Transitions for display as Front-End Form Actions - * - * @return FieldList - */ - public function getFrontEndWorkflowActions() { - $action = $this->CurrentAction(); - $options = $action->getValidTransitions(); - $actions = new FieldList(); - - $hide_disabled_actions_on_frontend = $this->config()->hide_disabled_actions_on_frontend; - - foreach ($options as $option) { - $btn = new FormAction("transition_{$option->ID}", $option->Title); - - // add cancel class to passive actions, this prevents js validation (using jquery.validate) - if($option->Type == 'Passive'){ - $btn->addExtraClass('cancel'); - } - - // disable the button if canExecute() returns false - if(!$option->canExecute($this)) - { - if ($hide_disabled_actions_on_frontend) - { - continue; - } - - $btn = $btn->performReadonlyTransformation(); - $btn->addExtraClass('hide'); - } - - $actions->push($btn); - } - - $action->updateFrontEndWorkflowActions($actions); - - return $actions; - } - - /** - * Gets Front-End DataObject - * - * @return DataObject - */ - public function getFrontEndDataObject() { - $action = $this->CurrentAction(); - $obj = $action->getFrontEndDataObject(); - - return $obj; - } - - /** - * Gets Front-End DataObject - * - * @return DataObject - */ - public function getFrontEndRequiredFields() { - $action = $this->CurrentAction(); - $validator = $action->getRequiredFields(); - - return $validator; - } - - public function setFrontendFormRequirements() { - $action = $this->CurrentAction(); - $action->setFrontendFormRequirements(); - } - - public function doFrontEndAction(array $data, Form $form, SS_HTTPRequest $request) { - $action = $this->CurrentAction(); - $action->doFrontEndAction($data, $form, $request); - } - - /* - * We need a way to "associate" an author with this WorkflowInstance and its Target() to see if she is "allowed" to view WorkflowInstances within GridFields - * @see {@link $this->userHasAccess()} - * - * @param number $recordID - * @param number $userID - * @param number $wasPublished - * @return boolean - */ - public function getVersionedConnection($recordID, $userID, $wasPublished = 0) { - // Turn this into an array and run through implode() - $filter = "RecordID = {$recordID} AND AuthorID = {$userID} AND WasPublished = {$wasPublished}"; - $query = new SQLSelect(); - $query->setFrom('"SiteTree_versions"')->setSelect('COUNT("ID")')->setWhere($filter); - $query->firstRow(); - $hasAuthored = $query->execute(); - if($hasAuthored) { - return true; - } - return false; - } - - /* - * Simple method to retrieve the current action, on the current WorkflowInstance - */ - public function getCurrentAction() { - $join = '"WorkflowAction"."ID" = "WorkflowActionInstance"."BaseActionID"'; - $action = WorkflowAction::get() - ->leftJoin('WorkflowActionInstance',$join) - ->where('"WorkflowActionInstance"."ID" = '.$this->CurrentActionID) - ->first(); - if(!$action) { - return 'N/A'; - } - return $action->getField('Title'); - } - - /** - * Tells us if $member has had permissions over some part of the current WorkflowInstance. - * - * @param $member - * @return \WorkflowAction | boolean - */ - public function getMostRecentActionForUser($member = null) { - if(!$member) { - if (!Member::currentUserID()) { - return false; - } - $member = Member::currentUser(); - } - - // WorkflowActionInstances in reverse creation-order so we get the most recent one's first - $history = $this->Actions()->filter(array( - 'Finished' =>1, - 'BaseAction.ClassName' => 'AssignUsersToWorkflowAction' - ))->Sort('Created', 'DESC'); - - $i=0; - foreach($history as $inst) { - /* + if (!$hasAccess) { + if ($this->getMostRecentActionForUser($member)) { + return true; + } + } + return $hasAccess; + } + + /** + * + * @param Member $member + * @return boolean + */ + public function canEdit($member = null) + { + $extended = $this->extendedCan(__FUNCTION__, $member); + if ($extended !== null) { + return $extended; + } + + return $this->userHasAccess($member); + } + + /** + * + * @param Member $member + * @return boolean + */ + public function canDelete($member = null) + { + $extended = $this->extendedCan(__FUNCTION__, $member); + if ($extended !== null) { + return $extended; + } + + if (Permission::checkMember($member, "DELETE_WORKFLOW")) { + return true; + } + return false; + } + + /** + * Checks whether the given user is in the list of users assigned to this + * workflow + * + * @param Member $member + */ + protected function userHasAccess($member) + { + if (!$member) { + if (!Security::getCurrentUser()) { + return false; + } + $member = Security::getCurrentUser(); + } + + if (Permission::checkMember($member, "ADMIN")) { + return true; + } + + // This method primarily "protects" access to a WorkflowInstance, but assumes access only to be granted to users assigned-to that WorkflowInstance. + // However; lowly authors (users entering items into a workflow) are not assigned - but we still wish them to see their submitted content. + $inWorkflowGroupOrUserTables = ($member->inGroups($this->Groups()) || $this->Users()->find('ID', $member->ID)); + // This method is used in more than just the ModelAdmin. Check for the current controller to determine where canView() expectations differ + if ($this->getTarget() && Controller::curr()->getAction() == 'index' && !$inWorkflowGroupOrUserTables) { + if ($this->getVersionedConnection($this->getTarget()->ID, $member->ID)) { + return true; + } + return false; + } + return $inWorkflowGroupOrUserTables; + } + + /** + * Can documents in the current workflow state be edited? + */ + public function canEditTarget() + { + if ($this->CurrentActionID && ($target = $this->getTarget())) { + return $this->CurrentAction()->canEditTarget($target); + } + } + + /** + * Does this action restrict viewing of the document? + * + * @return boolean + */ + public function canViewTarget() + { + $action = $this->CurrentAction(); + if ($action) { + return $action->canViewTarget($this->getTarget()); + } + return true; + } + + /** + * Does this action restrict the publishing of a document? + * + * @return boolean + */ + public function canPublishTarget() + { + if ($this->CurrentActionID && ($target = $this->getTarget())) { + return $this->CurrentAction()->canPublishTarget($target); + } + } + + /** + * Get the current set of transitions that are valid for the current workflow state, + * and are available to the current user. + * + * @return array + */ + public function validTransitions() + { + $action = $this->CurrentAction(); + $transitions = $action->getValidTransitions(); + + // Filter by execute permission + return $transitions->filterByCallback(function ($transition) { + return $transition->canExecute($this); + }); + } + + /* UI RELATED METHODS */ + + /** + * Gets fields for managing this workflow instance in its current step + * + * @return FieldList + */ + public function getWorkflowFields() + { + $action = $this->CurrentAction(); + $options = $this->validTransitions(); + $wfOptions = $options->map('ID', 'Title', ' '); + $fields = new FieldList(); + + $fields->push(new HeaderField('WorkflowHeader', $action->Title)); + + $fields->push(HiddenField::create('TransitionID', '')); + // Let the Active Action update the fields that the user can interact with so that data can be + // stored for the workflow. + $action->updateWorkflowFields($fields); + $action->invokeWithExtensions('updateWorkflowFields', $fields); + return $fields; + } + + /** + * Gets Front-End form fields from current Action + * + * @return FieldList + */ + public function getFrontEndWorkflowFields() + { + $action = $this->CurrentAction(); + + $fields = new FieldList(); + $action->updateFrontEndWorkflowFields($fields); + + return $fields; + } + + /** + * Gets Transitions for display as Front-End Form Actions + * + * @return FieldList + */ + public function getFrontEndWorkflowActions() + { + $action = $this->CurrentAction(); + $options = $action->getValidTransitions(); + $actions = new FieldList(); + + $hide_disabled_actions_on_frontend = $this->config()->hide_disabled_actions_on_frontend; + + foreach ($options as $option) { + $btn = new FormAction("transition_{$option->ID}", $option->Title); + + // add cancel class to passive actions, this prevents js validation (using jquery.validate) + if ($option->Type == 'Passive') { + $btn->addExtraClass('cancel'); + } + + // disable the button if canExecute() returns false + if (!$option->canExecute($this)) { + if ($hide_disabled_actions_on_frontend) { + continue; + } + + $btn = $btn->performReadonlyTransformation(); + $btn->addExtraClass('hide'); + } + + $actions->push($btn); + } + + $action->updateFrontEndWorkflowActions($actions); + + return $actions; + } + + /** + * Gets Front-End DataObject + * + * @return DataObject + */ + public function getFrontEndDataObject() + { + $action = $this->CurrentAction(); + $obj = $action->getFrontEndDataObject(); + + return $obj; + } + + /** + * Gets Front-End DataObject + * + * @return DataObject + */ + public function getFrontEndRequiredFields() + { + $action = $this->CurrentAction(); + $validator = $action->getRequiredFields(); + + return $validator; + } + + public function setFrontendFormRequirements() + { + $action = $this->CurrentAction(); + $action->setFrontendFormRequirements(); + } + + public function doFrontEndAction(array $data, Form $form, HTTPRequest $request) + { + $action = $this->CurrentAction(); + $action->doFrontEndAction($data, $form, $request); + } + + /** + * We need a way to "associate" an author with this WorkflowInstance and its Target() to see if she is "allowed" to view WorkflowInstances within GridFields + * @see {@link $this->userHasAccess()} + * + * @param number $recordID + * @param number $userID + * @param number $wasPublished + * @return boolean + */ + public function getVersionedConnection($recordID, $userID, $wasPublished = 0) + { + // Turn this into an array and run through implode() + $filter = "RecordID = {$recordID} AND AuthorID = {$userID} AND WasPublished = {$wasPublished}"; + $query = new SQLSelect(); + $query->setFrom('"SiteTree_Versions"')->setSelect('COUNT("ID")')->setWhere($filter); + $query->firstRow(); + $hasAuthored = $query->execute(); + if ($hasAuthored) { + return true; + } + return false; + } + + /** + * Simple method to retrieve the current action, on the current WorkflowInstance + */ + public function getCurrentAction() + { + $join = '"WorkflowAction"."ID" = "WorkflowActionInstance"."BaseActionID"'; + $action = WorkflowAction::get() + /** @skipUpgrade */ + ->leftJoin('WorkflowActionInstance', $join) + ->where('"WorkflowActionInstance"."ID" = '.$this->CurrentActionID) + ->first(); + if (!$action) { + return 'N/A'; + } + return $action->getField('Title'); + } + + /** + * Tells us if $member has had permissions over some part of the current WorkflowInstance. + * + * @param $member + * @return WorkflowAction|boolean + */ + public function getMostRecentActionForUser($member = null) + { + if (!$member) { + if (!Security::getCurrentUser()) { + return false; + } + $member = Security::getCurrentUser(); + } + + // WorkflowActionInstances in reverse creation-order so we get the most recent one's first + $history = $this->Actions()->filter(array( + 'Finished' =>1, + 'BaseAction.ClassName' => AssignUsersToWorkflowAction::class + ))->Sort('Created', 'DESC'); + + $i = 0; + foreach ($history as $inst) { + /* * This iteration represents the 1st instance in the list - the most recent AssignUsersToWorkflowAction in $history. * If there's no match for $member here or on the _previous_ AssignUsersToWorkflowAction, then bail out: */ - $assignedMembers = $inst->BaseAction()->getAssignedMembers(); - if($i<=1 && $assignedMembers->count()>0 && $assignedMembers->find('ID', $member->ID)) { - return $inst; - } - ++$i; - } - return false; - } + $assignedMembers = $inst->BaseAction()->getAssignedMembers(); + if ($i <= 1 && $assignedMembers->count() > 0 && $assignedMembers->find('ID', $member->ID)) { + return $inst; + } + ++$i; + } + return false; + } } diff --git a/code/dataobjects/WorkflowTransition.php b/code/dataobjects/WorkflowTransition.php index 3b226a04..c3214e51 100644 --- a/code/dataobjects/WorkflowTransition.php +++ b/code/dataobjects/WorkflowTransition.php @@ -1,10 +1,21 @@ 'Varchar(128)', + 'Sort' => 'Int', + 'Type' => "Enum('Active, Passive', 'Active')" + ); - private static $db = array( - 'Title' => 'Varchar(128)', - 'Sort' => 'Int', - 'Type' => "Enum('Active, Passive', 'Active')" - ); + private static $default_sort = 'Sort'; - private static $default_sort = 'Sort'; + private static $has_one = array( + 'Action' => WorkflowAction::class, + 'NextAction' => WorkflowAction::class, + ); - private static $has_one = array( - 'Action' => 'WorkflowAction', - 'NextAction' => 'WorkflowAction', - ); + private static $many_many = array( + 'Users' => Member::class, + 'Groups' => Group::class, + ); - private static $many_many = array( - 'Users' => 'SilverStripe\\Security\\Member', - 'Groups' => 'SilverStripe\\Security\\Group' - ); + private static $icon = 'advancedworkflow/images/transition.png'; - private static $icon = 'advancedworkflow/images/transition.png'; + private static $table_name = 'WorkflowTransition'; - /** - * - * @var array $extendedMethodReturn A basic extended validation routine method return format - */ - public static $extendedMethodReturn = array( - 'fieldName' =>null, - 'fieldField'=>null, - 'fieldMsg' =>null, - 'fieldValid'=>true - ); - - /** - * Returns true if it is valid for this transition to be followed given the - * current state of a workflow. - * - * @param WorkflowInstance $workflow - * @return bool - */ - public function isValid(WorkflowInstance $workflow) { - return true; - } + /** + * + * @var array $extendedMethodReturn A basic extended validation routine method return format + */ + public static $extendedMethodReturn = array( + 'fieldName' => null, + 'fieldField' => null, + 'fieldMsg' => null, + 'fieldValid' => true, + ); - /** - * Before saving, make sure we're not in an infinite loop - */ - public function onBeforeWrite() { - if(!$this->Sort) { - $this->Sort = DB::query('SELECT MAX("Sort") + 1 FROM "WorkflowTransition"')->value(); - } - - parent::onBeforeWrite(); - } - - public function validate() { - $result = parent::validate(); - return $result; - } - - /* CMS FUNCTIONS */ - - public function getCMSFields() { - $fields = new FieldList(new TabSet('Root')); - $fields->addFieldToTab('Root.Main', new TextField('Title', $this->fieldLabel('Title'))); - - $filter = ''; - - $reqParent = isset($_REQUEST['ParentID']) ? (int) $_REQUEST['ParentID'] : 0; - $attachTo = $this->ActionID ? $this->ActionID : $reqParent; - - if ($attachTo) { - $action = DataObject::get_by_id('WorkflowAction', $attachTo); - if ($action && $action->ID) { - $filter = '"WorkflowDefID" = '.((int) $action->WorkflowDefID); - } - } - - $actions = DataObject::get('WorkflowAction', $filter); - $options = array(); - if ($actions) { - $options = $actions->map(); - } - - $defaultAction = $action?$action->ID:""; - - $typeOptions = array( - 'Active' => _t('WorkflowTransition.Active', 'Active'), - 'Passive' => _t('WorkflowTransition.Passive', 'Passive'), - ); - - $fields->addFieldToTab('Root.Main', new DropdownField( - 'ActionID', - $this->fieldLabel('ActionID'), - $options, $defaultAction)); - $fields->addFieldToTab('Root.Main', $nextActionDropdownField = new DropdownField( - 'NextActionID', - $this->fieldLabel('NextActionID'), - $options)); - $nextActionDropdownField->setEmptyString(_t('WorkflowTransition.SELECTONE', '(Select one)')); - $fields->addFieldToTab('Root.Main', new DropdownField( - 'Type', - _t('WorkflowTransition.TYPE', 'Type'), - $typeOptions - )); - - $members = Member::get(); - $fields->findOrMakeTab( - 'Root.RestrictToUsers', - _t('WorkflowTransition.TabTitle', 'Restrict to users') - ); - $fields->addFieldToTab('Root.RestrictToUsers', new CheckboxSetField('Users', _t('WorkflowDefinition.USERS', 'Restrict to Users'), $members)); - $fields->addFieldToTab('Root.RestrictToUsers', new TreeMultiselectField('Groups', _t('WorkflowDefinition.GROUPS', 'Restrict to Groups'), 'SilverStripe\\Security\\Group')); - - $this->extend('updateCMSFields', $fields); - - return $fields; - } - - public function fieldLabels($includerelations = true) { - $labels = parent::fieldLabels($includerelations); - $labels['Title'] = _t('WorkflowAction.TITLE', 'Title'); - $labels['ActionID'] = _t('WorkflowTransition.ACTION', 'Action'); - $labels['NextActionID'] = _t('WorkflowTransition.NEXT_ACTION', 'Next Action'); - - return $labels; - } - - public function getValidator() { - $required = new AWRequiredFields('Title', 'ActionID', 'NextActionID'); - $required->setCaller($this); - return $required; - } - - public function numChildren() { - return 0; - } - - public function summaryFields() { - return array( - 'Title' => $this->fieldLabel('Title') - ); - } - - - /** - * Check if the current user can execute this transition - * - * @return bool - **/ - public function canExecute(WorkflowInstance $workflow){ - $return = true; - $members = $this->getAssignedMembers(); - - // If not admin, check if the member is in the list of assigned members - if(!Permission::check('ADMIN') && $members->exists()){ - if(!$members->find('ID', Member::currentUserID())) { - $return = false; - } - } - - if($return) { - $extended = $this->extend('extendCanExecute', $workflow); - if($extended) $return = min($extended); - } - - return $return !== false; - } + /** + * Returns true if it is valid for this transition to be followed given the + * current state of a workflow. + * + * @param WorkflowInstance $workflow + * @return bool + */ + public function isValid(WorkflowInstance $workflow) + { + return true; + } + + /** + * Before saving, make sure we're not in an infinite loop + */ + public function onBeforeWrite() + { + if (!$this->Sort) { + $this->Sort = DB::query('SELECT MAX("Sort") + 1 FROM "WorkflowTransition"')->value(); + } + + parent::onBeforeWrite(); + } + + /* CMS FUNCTIONS */ + + public function getCMSFields() + { + $fields = new FieldList(new TabSet('Root')); + $fields->addFieldToTab('Root.Main', new TextField('Title', $this->fieldLabel('Title'))); + + $filter = ''; + + $reqParent = isset($_REQUEST['ParentID']) ? (int) $_REQUEST['ParentID'] : 0; + $attachTo = $this->ActionID ? $this->ActionID : $reqParent; + + if ($attachTo) { + $action = DataObject::get_by_id(WorkflowAction::class, $attachTo); + if ($action && $action->ID) { + $filter = '"WorkflowDefID" = '.((int) $action->WorkflowDefID); + } + } + + $actions = DataObject::get(WorkflowAction::class, $filter); + $options = array(); + if ($actions) { + $options = $actions->map(); + } + + $defaultAction = $action ? $action->ID : ""; + + $typeOptions = array( + 'Active' => _t('WorkflowTransition.Active', 'Active'), + 'Passive' => _t('WorkflowTransition.Passive', 'Passive'), + ); + + $fields->addFieldToTab('Root.Main', new DropdownField( + 'ActionID', + $this->fieldLabel('ActionID'), + $options, + $defaultAction + )); + $fields->addFieldToTab('Root.Main', $nextActionDropdownField = new DropdownField( + 'NextActionID', + $this->fieldLabel('NextActionID'), + $options + )); + $nextActionDropdownField->setEmptyString(_t('WorkflowTransition.SELECTONE', '(Select one)')); + $fields->addFieldToTab('Root.Main', new DropdownField( + 'Type', + _t('WorkflowTransition.TYPE', 'Type'), + $typeOptions + )); + + $members = Member::get(); + $fields->findOrMakeTab( + 'Root.RestrictToUsers', + _t('WorkflowTransition.TabTitle', 'Restrict to users') + ); + $fields->addFieldToTab('Root.RestrictToUsers', new CheckboxSetField('Users', _t('WorkflowDefinition.USERS', 'Restrict to Users'), $members)); + $fields->addFieldToTab('Root.RestrictToUsers', new TreeMultiselectField('Groups', _t('WorkflowDefinition.GROUPS', 'Restrict to Groups'), Group::class)); + + $this->extend('updateCMSFields', $fields); + + return $fields; + } + + public function fieldLabels($includerelations = true) + { + $labels = parent::fieldLabels($includerelations); + $labels['Title'] = _t('WorkflowAction.TITLE', 'Title'); + $labels['ActionID'] = _t('WorkflowTransition.ACTION', 'Action'); + $labels['NextActionID'] = _t('WorkflowTransition.NEXT_ACTION', 'Next Action'); + + return $labels; + } + + public function getValidator() + { + $required = new AWRequiredFields('Title', 'ActionID', 'NextActionID'); + $required->setCaller($this); + return $required; + } + + public function numChildren() + { + return 0; + } + + public function summaryFields() + { + return array( + 'Title' => $this->fieldLabel('Title') + ); + } + + + /** + * Check if the current user can execute this transition + * + * @return bool + **/ + public function canExecute(WorkflowInstance $workflow) + { + $return = true; + $members = $this->getAssignedMembers(); + + // If not admin, check if the member is in the list of assigned members + if (!Permission::check('ADMIN') && $members->exists()) { + if (!$members->find('ID', Security::getCurrentUser()->ID)) { + $return = false; + } + } + + if ($return) { + $extended = $this->extend('extendCanExecute', $workflow); + if ($extended) { + $return = min($extended); + } + } + + return $return !== false; + } /** * Allows users who have permission to create a WorkflowDefinition, to create actions on it too. @@ -198,65 +219,71 @@ public function canExecute(WorkflowInstance $workflow){ * @param array $context * @return bool */ - public function canCreate($member = null, $context = array()) { - return $this->Action()->WorkflowDef()->canCreate($member, $context); - } + public function canCreate($member = null, $context = array()) + { + return $this->Action()->WorkflowDef()->canCreate($member, $context); + } - /** - * @param Member $member - * @return bool - */ - public function canEdit($member = null) { - return $this->canCreate($member); - } + /** + * @param Member $member + * @return bool + */ + public function canEdit($member = null) + { + return $this->canCreate($member); + } - /** - * @param Member $member - * @return bool - */ - public function canDelete($member = null) { - return $this->canCreate($member); - } + /** + * @param Member $member + * @return bool + */ + public function canDelete($member = null) + { + return $this->canCreate($member); + } - /** - * Returns a set of all Members that are assigned to this transition, either directly or via a group. - * - * @return ArrayList - */ - public function getAssignedMembers() { - $members = ArrayList::create($this->Users()->toArray()); - $groups = $this->Groups(); + /** + * Returns a set of all Members that are assigned to this transition, either directly or via a group. + * + * @return ArrayList + */ + public function getAssignedMembers() + { + $members = ArrayList::create($this->Users()->toArray()); + $groups = $this->Groups(); - foreach($groups as $group) { - $members->merge($group->Members()); - } + foreach ($groups as $group) { + $members->merge($group->Members()); + } - $members->removeDuplicates(); - return $members; - } + $members->removeDuplicates(); + return $members; + } - /* + /* * A simple field same-value checker. * * @param array $data * @return array * @see {@link AWRequiredFields} */ - public function extendedRequiredFieldsNotSame($data=null) { - $check = array('ActionID','NextActionID'); - foreach($check as $fieldName) { - if(!isset($data[$fieldName])) { - return self::$extendedMethodReturn; - } - } - // Have we found some identical values? - if($data[$check[0]] == $data[$check[1]]) { - self::$extendedMethodReturn['fieldName'] = $check[0]; // Used to display to the user, so the first of the array is fine - self::$extendedMethodReturn['fieldValid'] = false; - self::$extendedMethodReturn['fieldMsg'] = _t( - 'WorkflowTransition.TRANSITIONLOOP', - 'A transition cannot lead back to its parent action.'); - } - return self::$extendedMethodReturn; - } + public function extendedRequiredFieldsNotSame($data = null) + { + $check = array('ActionID','NextActionID'); + foreach ($check as $fieldName) { + if (!isset($data[$fieldName])) { + return self::$extendedMethodReturn; + } + } + // Have we found some identical values? + if ($data[$check[0]] == $data[$check[1]]) { + self::$extendedMethodReturn['fieldName'] = $check[0]; // Used to display to the user, so the first of the array is fine + self::$extendedMethodReturn['fieldValid'] = false; + self::$extendedMethodReturn['fieldMsg'] = _t( + 'WorkflowTransition.TRANSITIONLOOP', + 'A transition cannot lead back to its parent action.' + ); + } + return self::$extendedMethodReturn; + } } diff --git a/code/dev/WorkflowBulkLoader.php b/code/dev/WorkflowBulkLoader.php index 5d053068..f728fa4c 100644 --- a/code/dev/WorkflowBulkLoader.php +++ b/code/dev/WorkflowBulkLoader.php @@ -1,7 +1,18 @@ processAll($filepath, true); - } - - /** - * @param string $filepath - * @param boolean $preview - */ - protected function processAll($filepath, $preview = false) { - $results = new BulkLoader_Result(); - - try { - $yml = singleton('WorkflowDefinitionImporter')->parseYAMLImport($filepath); - $this->processRecord($yml, $this->columnMap, $results, $preview); - return $results; - } catch(ValidationException $e) { - return new BulkLoader_Result(); - } - } - - /** - * @param array $record - * @param array $columnMap - * @param BulkLoader_Result $results - * @param boolean $preview - * @return number - */ - protected function processRecord($record, $columnMap, &$results, $preview = false) { - $posted = Controller::curr()->getRequest()->postVars(); - $default = WorkflowDefinitionExporter::$export_filename_prefix.'0.yml'; - $filename = (isset($posted['_CsvFile']['name']) ? $posted['_CsvFile']['name'] : $default); - - // @todo is this the best way to extract records (nested array keys)?? - $struct = $record['Injector']['ExportedWorkflow']; - $name = $struct['constructor'][0]; - $import = $this->createImport($name, $filename, $record); - - $template = Injector::inst()->createWithArgs('WorkflowTemplate', $struct['constructor']); - $template->setStructure($struct['properties']['structure']); +class WorkflowBulkLoader extends BulkLoader +{ + /** + * @inheritDoc + */ + public function preview($filepath) + { + return $this->processAll($filepath, true); + } + + /** + * @param string $filepath + * @param boolean $preview + */ + protected function processAll($filepath, $preview = false) + { + $results = new BulkLoader_Result(); + + try { + $yml = singleton(WorkflowDefinitionImporter::class)->parseYAMLImport($filepath); + $this->processRecord($yml, $this->columnMap, $results, $preview); + return $results; + } catch (ValidationException $e) { + return new BulkLoader_Result(); + } + } + + /** + * @param array $record + * @param array $columnMap + * @param BulkLoader_Result $results + * @param boolean $preview + * @return number + */ + protected function processRecord($record, $columnMap, &$results, $preview = false) + { + $posted = Controller::curr()->getRequest()->postVars(); + $default = WorkflowDefinitionExporter::$export_filename_prefix.'0.yml'; + $filename = (isset($posted['_CsvFile']['name']) ? $posted['_CsvFile']['name'] : $default); + + // @todo is this the best way to extract records (nested array keys)?? + $struct = $record[Injector::class]['ExportedWorkflow']; + $name = $struct['constructor'][0]; + $import = $this->createImport($name, $filename, $record); + + $template = Injector::inst()->createWithArgs(WorkflowTemplate::class, $struct['constructor']); + $template->setStructure($struct['properties']['structure']); + + $def = WorkflowDefinition::create(); + $def->workflowService = singleton(WorkflowService::class); + $def->Template = $template->getName(); + $obj = $def->workflowService->defineFromTemplate($def, $def->Template); + + $results->addCreated($obj, ''); + $objID = $obj->ID; + + // Update the import + $import->DefinitionID = $objID; + $import->write(); + + $obj->destroy(); + unset($obj); + + return $objID; + } + + /** + * Create the ImportedWorkflowTemplate record for the uploaded YML file. + * + * @param string $name + * @param string $filename + * @param array $record + * @return ImportedWorkflowTemplate $import + */ + protected function createImport($name, $filename, $record) + { + // This is needed to feed WorkflowService#getNamedTemplate() + $import = ImportedWorkflowTemplate::create(); + $import->Name = $name; + $import->Filename = $filename; + $import->Content = serialize($record); + $import->write(); - $def = WorkflowDefinition::create(); - $def->workflowService = singleton('WorkflowService'); - $def->Template = $template->getName(); - $obj = $def->workflowService->defineFromTemplate($def, $def->Template); - - $results->addCreated($obj, ''); - $objID = $obj->ID; - - // Update the import - $import->DefinitionID = $objID; - $import->write(); - - $obj->destroy(); - unset($obj); - - return $objID; - } - - /** - * Create the ImportedWorkflowTemplate record for the uploaded YML file. - * - * @param string $name - * @param string $filename - * @param array $record - * @return ImportedWorkflowTemplate $import - */ - protected function createImport($name, $filename, $record) { - // This is needed to feed WorkflowService#getNamedTemplate() - $import = ImportedWorkflowTemplate::create(); - $import->Name = $name; - $import->Filename = $filename; - $import->Content = serialize($record); - $import->write(); - - return $import; - } + return $import; + } } diff --git a/code/extensions/AdvancedWorkflowExtension.php b/code/extensions/AdvancedWorkflowExtension.php index 08420690..96a1d39a 100644 --- a/code/extensions/AdvancedWorkflowExtension.php +++ b/code/extensions/AdvancedWorkflowExtension.php @@ -1,155 +1,169 @@ getRecord(); - $workflowID = isset($data['TriggeredWorkflowID']) ? intval($data['TriggeredWorkflowID']) : 0; - - if (!$item || !$item->canEdit()) { - return; - } - - // Save a draft, if the user forgets to do so - $this->saveAsDraftWithAction($form, $item); - - $svc = singleton('WorkflowService'); - $svc->startWorkflow($item, $workflowID); - - return $this->returnResponse($form); - } - - /** - * Need to update the edit form AFTER it's been transformed to read only so that the workflow stuff is still - * allowed to be added with 'write' permissions - * - * @param Form $form - */ - public function updateEditForm(Form $form) { - Requirements::javascript(ADVANCED_WORKFLOW_DIR . '/javascript/advanced-workflow-cms.js'); - $svc = singleton('WorkflowService'); - $p = $form->getRecord(); - $active = $svc->getWorkflowFor($p); - - if ($active) { - - $fields = $form->Fields(); - $current = $active->CurrentAction(); - $wfFields = $active->getWorkflowFields(); - - $allowed = array_keys($wfFields->saveableFields()); - $data = array(); - foreach ($allowed as $fieldName) { - $data[$fieldName] = $current->$fieldName; - } - - $fields->findOrMakeTab( - 'Root.WorkflowActions', - _t('Workflow.WorkflowActionsTabTitle', 'Workflow Actions') - ); - $fields->addFieldsToTab('Root.WorkflowActions', $wfFields); - - $form->loadDataFrom($data); - - if (!$p->canEditWorkflow()) { - $form->makeReadonly(); - } - - $this->owner->extend('updateWorkflowEditForm', $form); - } - } - - public function updateItemEditForm($form) { - $record = $form->getRecord(); - if ($record && $record->hasExtension('WorkflowApplicable')) { - $actions = $form->Actions(); - $record->extend('updateCMSActions', $actions); - $this->updateEditForm($form); - } - } - - /** - * Update a workflow based on user input. - * - * @todo refactor with WorkflowInstance::updateWorkflow - * - * @param array $data - * @param Form $form - * @param SS_HTTPRequest $request - * @return String - */ - public function updateworkflow($data, Form $form, $request) { - $svc = singleton('WorkflowService'); - $p = $form->getRecord(); - $workflow = $svc->getWorkflowFor($p); - $action = $workflow->CurrentAction(); - - if (!$p || !$p->canEditWorkflow()) { - return; - } - - $allowedFields = $workflow->getWorkflowFields()->saveableFields(); - unset($allowedFields['TransitionID']); - - $allowed = array_keys($allowedFields); - if (count($allowed)) { - $form->saveInto($action, $allowed); - $action->write(); - } - - if (isset($data['TransitionID']) && $data['TransitionID']) { - $svc->executeTransition($p, $data['TransitionID']); - } else { - // otherwise, just try to execute the current workflow to see if it - // can now proceed based on user input - $workflow->execute(); - } - - return $this->returnResponse($form); - } - - protected function returnResponse($form) { - if ($this->owner instanceof GridFieldDetailForm_ItemRequest) { - $record = $form->getRecord(); - if ($record && $record->exists()) { - return $this->owner->edit($this->owner->getRequest()); - } - } - - $negotiator = method_exists($this->owner, 'getResponseNegotiator') ? $this->owner->getResponseNegotiator() : Controller::curr()->getResponseNegotiator(); - return $negotiator->respond($this->owner->getRequest()); - } - - /** - * Ocassionally users forget to apply their changes via the standard CMS "Save Draft" button, - * and select the action button instead - losing their changes. - * Calling this from a controller method saves a draft automatically for the user, whenever a workflow action is run. - * See: #72 and #77 - * - * @param \Form $form - * @param \DataObject $item - * @return void - */ - protected function saveAsDraftWithAction(Form $form, DataObject $item) { - $form->saveInto($item); - $item->write(); - } - +class AdvancedWorkflowExtension extends Extension +{ + private static $allowed_actions = array( + 'updateworkflow', + 'startworkflow' + ); + + public function startworkflow($data, $form, $request) + { + $item = $form->getRecord(); + $workflowID = isset($data['TriggeredWorkflowID']) ? intval($data['TriggeredWorkflowID']) : 0; + + if (!$item || !$item->canEdit()) { + return; + } + + // Save a draft, if the user forgets to do so + $this->saveAsDraftWithAction($form, $item); + + $svc = singleton(WorkflowService::class); + $svc->startWorkflow($item, $workflowID); + + return $this->returnResponse($form); + } + + /** + * Need to update the edit form AFTER it's been transformed to read only so that the workflow stuff is still + * allowed to be added with 'write' permissions + * + * @param Form $form + */ + public function updateEditForm(Form $form) + { + $module = ModuleLoader::getModule('symbiote/silverstripe-advancedworkflow'); + Requirements::javascript($module->getRelativeResourcePath('javascript/advanced-workflow-cms.js')); + $svc = singleton(WorkflowService::class); + $p = $form->getRecord(); + $active = $svc->getWorkflowFor($p); + + if ($active) { + $fields = $form->Fields(); + $current = $active->CurrentAction(); + $wfFields = $active->getWorkflowFields(); + + $allowed = array_keys($wfFields->saveableFields()); + $data = array(); + foreach ($allowed as $fieldName) { + $data[$fieldName] = $current->$fieldName; + } + + $fields->findOrMakeTab( + 'Root.WorkflowActions', + _t('Workflow.WorkflowActionsTabTitle', 'Workflow Actions') + ); + $fields->addFieldsToTab('Root.WorkflowActions', $wfFields); + + $form->loadDataFrom($data); + + if (!$p->canEditWorkflow()) { + $form->makeReadonly(); + } + + $this->owner->extend('updateWorkflowEditForm', $form); + } + } + + public function updateItemEditForm($form) + { + $record = $form->getRecord(); + if ($record && $record->hasExtension(WorkflowApplicable::class)) { + $actions = $form->Actions(); + $record->extend('updateCMSActions', $actions); + $this->updateEditForm($form); + } + } + + /** + * Update a workflow based on user input. + * + * @todo refactor with WorkflowInstance::updateWorkflow + * + * @param array $data + * @param Form $form + * @param HTTPRequest $request + * @return string + */ + public function updateworkflow($data, Form $form, $request) + { + $svc = singleton(WorkflowService::class); + $p = $form->getRecord(); + $workflow = $svc->getWorkflowFor($p); + $action = $workflow->CurrentAction(); + + if (!$p || !$p->canEditWorkflow()) { + return; + } + + $allowedFields = $workflow->getWorkflowFields()->saveableFields(); + unset($allowedFields['TransitionID']); + + $allowed = array_keys($allowedFields); + if (count($allowed)) { + $form->saveInto($action, $allowed); + $action->write(); + } + + if (isset($data['TransitionID']) && $data['TransitionID']) { + $svc->executeTransition($p, $data['TransitionID']); + } else { + // otherwise, just try to execute the current workflow to see if it + // can now proceed based on user input + $workflow->execute(); + } + + return $this->returnResponse($form); + } + + protected function returnResponse($form) + { + if ($this->owner instanceof GridFieldDetailForm_ItemRequest) { + $record = $form->getRecord(); + if ($record && $record->exists()) { + return $this->owner->edit($this->owner->getRequest()); + } + } + + $negotiator = method_exists($this->owner, 'getResponseNegotiator') ? $this->owner->getResponseNegotiator() : Controller::curr()->getResponseNegotiator(); + return $negotiator->respond($this->owner->getRequest()); + } + + /** + * Ocassionally users forget to apply their changes via the standard CMS "Save Draft" button, + * and select the action button instead - losing their changes. + * Calling this from a controller method saves a draft automatically for the user, whenever a workflow action is run. + * See: #72 and #77 + * + * @param Form $form + * @param DataObject $item + * @return void + */ + protected function saveAsDraftWithAction(Form $form, DataObject $item) + { + $form->saveInto($item); + $item->write(); + } } diff --git a/code/extensions/FileWorkflowApplicable.php b/code/extensions/FileWorkflowApplicable.php index 52b23dad..b7afee8b 100644 --- a/code/extensions/FileWorkflowApplicable.php +++ b/code/extensions/FileWorkflowApplicable.php @@ -1,83 +1,89 @@ owner->ID) { + return $fields; + } + parent::updateCMSFields($fields); + + // add the workflow fields directly. It's a requirement of workflow on file objects + // that CMS admins mark the workflow step as being editable for files to be administerable + $active = $this->workflowService->getWorkflowFor($this->owner); + if ($active) { + $current = $active->CurrentAction(); + $wfFields = $active->getWorkflowFields(); + + // loading data in a somewhat hack way + $form = new Form($this, 'DummyForm', $wfFields, new FieldList()); + $form->loadDataFrom($current); + + $fields->findOrMakeTab( + 'Root.WorkflowActions', + _t('Workflow.WorkflowActionsTabTitle', 'Workflow Actions') + ); + $fields->addFieldsToTab('Root.WorkflowActions', $wfFields); + } + } - public function updateCMSFields(FieldList $fields) { - if (!$this->owner->ID) { - return $fields; - } - parent::updateCMSFields($fields); + public function onAfterWrite() + { + parent::onAfterWrite(); - // add the workflow fields directly. It's a requirement of workflow on file objects - // that CMS admins mark the workflow step as being editable for files to be administerable - $active = $this->workflowService->getWorkflowFor($this->owner); - if ($active) { - $current = $active->CurrentAction(); - $wfFields = $active->getWorkflowFields(); - - // loading data in a somewhat hack way - $form = new Form($this, 'DummyForm', $wfFields, new FieldList()); - $form->loadDataFrom($current); + $workflow = $this->workflowService->getWorkflowFor($this->owner); + $rawData = $this->owner->toMap(); + if ($workflow && $this->owner->TransitionID) { + // we want to transition, so do so if that's a valid transition to take. + $action = $workflow->CurrentAction(); + if (!$this->canEditWorkflow()) { + return; + } - $fields->findOrMakeTab( - 'Root.WorkflowActions', - _t('Workflow.WorkflowActionsTabTitle', 'Workflow Actions') - ); - $fields->addFieldsToTab('Root.WorkflowActions', $wfFields); - } - } + $allowedFields = $workflow->getWorkflowFields()->saveableFields(); + unset($allowedFields['TransitionID']); - public function onAfterWrite() { - parent::onAfterWrite(); - - $workflow = $this->workflowService->getWorkflowFor($this->owner); - $rawData = $this->owner->toMap(); - if ($workflow && $this->owner->TransitionID) { - // we want to transition, so do so if that's a valid transition to take. - $action = $workflow->CurrentAction(); - if (!$this->canEditWorkflow()) { - return; - } + $allowed = array_keys($allowedFields); - $allowedFields = $workflow->getWorkflowFields()->saveableFields(); - unset($allowedFields['TransitionID']); + foreach ($allowed as $field) { + if (isset($rawData[$field])) { + $action->$field = $rawData[$field]; + } + } - $allowed = array_keys($allowedFields); - - foreach ($allowed as $field) { - if (isset($rawData[$field])) { - $action->$field = $rawData[$field]; - } - } - - $action->write(); + $action->write(); - if (isset($rawData['TransitionID']) && $rawData['TransitionID']) { - // unset the transition ID so this doesn't get re-executed - $this->owner->TransitionID = null; - $this->workflowService->executeTransition($this->owner, $rawData['TransitionID']); - } else { - // otherwise, just try to execute the current workflow to see if it - // can now proceed based on user input - $workflow->execute(); - } - } - } -} \ No newline at end of file + if (isset($rawData['TransitionID']) && $rawData['TransitionID']) { + // unset the transition ID so this doesn't get re-executed + $this->owner->TransitionID = null; + $this->workflowService->executeTransition($this->owner, $rawData['TransitionID']); + } else { + // otherwise, just try to execute the current workflow to see if it + // can now proceed based on user input + $workflow->execute(); + } + } + } +} diff --git a/code/extensions/WorkflowApplicable.php b/code/extensions/WorkflowApplicable.php index 091ba795..07453c03 100644 --- a/code/extensions/WorkflowApplicable.php +++ b/code/extensions/WorkflowApplicable.php @@ -1,9 +1,30 @@ 'WorkflowDefinition', - ); - - private static $many_many = array( - 'AdditionalWorkflowDefinitions' => 'WorkflowDefinition' - ); - - private static $dependencies = array( - 'workflowService' => '%$WorkflowService', - ); - - /** - * - * Used to flag to this extension if there's a WorkflowPublishTargetJob running. - * @var boolean - */ - public $isPublishJobRunning = false; - - /** - * - * @param boolean $truth - */ - public function setIsPublishJobRunning($truth) { - $this->isPublishJobRunning = $truth; - } - - /** - * - * @return boolean - */ - public function getIsPublishJobRunning() { - return $this->isPublishJobRunning; - } - - /** - * - * @see {@link $this->isPublishJobRunning} - * @return boolean - */ - public function isPublishJobRunning() { - $propIsSet = $this->getIsPublishJobRunning() ? true : false; - return class_exists('AbstractQueuedJob') && $propIsSet; - } - - /** - * @var WorkflowService - */ - public $workflowService; - - /** - * - * A cache var for the current workflow instance - * - * @var WorkflowInstance - */ - protected $currentInstance; - - public function updateSettingsFields(FieldList $fields) { - $this->updateFields($fields); - } - - public function updateCMSFields(FieldList $fields) { - if(!$this->owner->hasMethod('getSettingsFields')) $this->updateFields($fields); - - // Instantiate a hidden form field to pass the triggered workflow definition through, allowing a dynamic form action. - - $fields->push(HiddenField::create( - 'TriggeredWorkflowID' - )); - } - - public function updateFields(FieldList $fields) { - if (!$this->owner->ID) { - return $fields; - } - - $tab = $fields->fieldByName('Root') ? $fields->findOrMakeTab('Root.Workflow') : $fields; - - if(Permission::check('APPLY_WORKFLOW')) { - $definition = new DropdownField('WorkflowDefinitionID', _t('WorkflowApplicable.DEFINITION', 'Applied Workflow')); - $definitions = $this->workflowService->getDefinitions()->map()->toArray(); - $definition->setSource($definitions); - $definition->setEmptyString(_t('WorkflowApplicable.INHERIT', 'Inherit from parent')); - $tab->push($definition); - - // Allow an optional selection of additional workflow definitions. - - if($this->owner->WorkflowDefinitionID) { - $fields->removeByName('AdditionalWorkflowDefinitions'); - unset($definitions[$this->owner->WorkflowDefinitionID]); - $tab->push($additional = ListboxField::create( - 'AdditionalWorkflowDefinitions', - _t('WorkflowApplicable.ADDITIONAL_WORKFLOW_DEFINITIONS', 'Additional Workflows') - )); - $additional->setSource($definitions); - } - } - - // Display the effective workflow definition. - - if($effective = $this->getWorkflowInstance()) { - $title = $effective->Definition()->Title; - $tab->push(ReadonlyField::create( - 'EffectiveWorkflow', - _t('WorkflowApplicable.EFFECTIVE_WORKFLOW', 'Effective Workflow'), - $title - )); - } - - if($this->owner->ID) { - $config = new GridFieldConfig_Base(); - $config->addComponent(new GridFieldEditButton()); - $config->addComponent(new GridFieldDetailForm()); - - $insts = $this->owner->WorkflowInstances(); - $log = new GridField('WorkflowLog', _t('WorkflowApplicable.WORKFLOWLOG', 'Workflow Log'), $insts, $config); - - $tab->push($log); - } - } - - public function updateCMSActions(FieldList $actions) { - $active = $this->workflowService->getWorkflowFor($this->owner); - $c = Controller::curr(); - if ($c && $c->hasExtension('AdvancedWorkflowExtension')) { - if ($active) { - if ($this->canEditWorkflow()) { - $workflowOptions = new Tab( - 'WorkflowOptions', - _t('SiteTree.WorkflowOptions', 'Workflow options', 'Expands a view for workflow specific buttons') - ); - - $menu = $actions->fieldByName('ActionMenus'); - if (!$menu) { - // create the menu for adding to any arbitrary non-sitetree object - $menu = $this->createActionMenu(); - $actions->push($menu); - } - - if(!$actions->fieldByName('ActionMenus.WorkflowOptions')) { - $menu->push($workflowOptions); - } - - $transitions = $active->CurrentAction()->getValidTransitions(); - - foreach ($transitions as $transition) { - if ($transition->canExecute($active)) { - $action = FormAction::create('updateworkflow-' . $transition->ID, $transition->Title) - ->setAttribute('data-transitionid', $transition->ID); - $workflowOptions->push($action); - } - } - - // $action = FormAction::create('updateworkflow', $active->CurrentAction() ? $active->CurrentAction()->Title : _t('WorkflowApplicable.UPDATE_WORKFLOW', 'Update Workflow')) - // ->setAttribute('data-icon', 'navigation'); - // $actions->fieldByName('MajorActions') ? $actions->fieldByName('MajorActions')->push($action) : $actions->push($action); - } - } else { - // Instantiate the workflow definition initial actions. - $definitions = $this->workflowService->getDefinitionsFor($this->owner); - if($definitions) { - $menu = $actions->fieldByName('ActionMenus'); - if(is_null($menu)) { - - // Instantiate a new action menu for any data objects. - - $menu = $this->createActionMenu(); - $actions->push($menu); - } - $tab = Tab::create( - 'AdditionalWorkflows' - ); - $addedFirst = false; - foreach($definitions as $definition) { - if($definition->getInitialAction() && $this->owner->canEdit()) { - $action = FormAction::create( - "startworkflow-{$definition->ID}", - $definition->InitialActionButtonText ? $definition->InitialActionButtonText : $definition->getInitialAction()->Title - )->addExtraClass('start-workflow')->setAttribute('data-workflow', $definition->ID); - - // The first element is the main workflow definition, and will be displayed as a major action. - if(!$addedFirst) { - $addedFirst = true; - $action->setAttribute('data-icon', 'navigation'); - $majorActions = $actions->fieldByName('MajorActions'); - $majorActions ? $majorActions->push($action) : $actions->push($action); - } else { - $tab->push($action); - } - } - } - // Only display menu if actions pushed to it - if ($tab->Fields()->exists()) { - $menu->insertBefore($tab, 'MoreOptions'); - } - } - } - } - } - - protected function createActionMenu() { - $rootTabSet = new TabSet('ActionMenus'); - $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus'); - return $rootTabSet; - } - - /** - * Included in CMS-generated email templates for a NotifyUsersWorkflowAction. - * Returns an absolute link to the CMS UI for a Page object - * - * @return string | null - */ - public function AbsoluteEditLink() { - $CMSEditLink = null; - - if($this->owner instanceof CMSPreviewable) { - $CMSEditLink = $this->owner->CMSEditLink(); - } else if ($this->owner->hasMethod('WorkflowLink')) { - $CMSEditLink = $this->owner->WorkflowLink(); - } - - if ($CMSEditLink === null) { - return null; - } - - return Controller::join_links(Director::absoluteBaseURL(), $CMSEditLink); - } - - /** - * Included in CMS-generated email templates for a NotifyUsersWorkflowAction. - * Allows users to select a link in an email for direct access to the transition-selection dropdown in the CMS UI. - * - * @return string - */ - public function LinkToPendingItems() { - $urlBase = Director::absoluteBaseURL(); - $urlFrag = 'admin/workflows/WorkflowDefinition/EditForm/field'; - $urlInst = $this->getWorkflowInstance(); - return Controller::join_links($urlBase, $urlFrag, 'PendingObjects', 'item', $urlInst->ID, 'edit'); - } - - /** - * After a workflow item is written, we notify the - * workflow so that it can take action if needbe - */ - public function onAfterWrite() { - $instance = $this->getWorkflowInstance(); - if ($instance && $instance->CurrentActionID) { - $action = $instance->CurrentAction()->BaseAction()->targetUpdated($instance); - } - } - - public function WorkflowInstances() { - return WorkflowInstance::get()->filter(array( - 'TargetClass' => $this->ownerBaseClass, - 'TargetID' => $this->owner->ID - )); - } - - /** - * Gets the current instance of workflow - * - * @return WorkflowInstance - */ - public function getWorkflowInstance() { - if (!$this->currentInstance) { - $this->currentInstance = $this->workflowService->getWorkflowFor($this->owner); - } - - return $this->currentInstance; - } - - - /** - * Gets the history of a workflow instance - * - * @return DataObjectSet - */ - public function getWorkflowHistory($limit = null) { - return $this->workflowService->getWorkflowHistoryFor($this->owner, $limit); - } - - /** - * Check all recent WorkflowActionIntances and return the most recent one with a Comment - * - * @return WorkflowActionInstance - */ - public function RecentWorkflowComment($limit = 10){ - if($actions = $this->getWorkflowHistory($limit)){ - foreach ($actions as $action) { - if ($action->Comment != '') { - return $action; - } - } - } - } - - /** - * Content can never be directly publishable if there's a workflow applied. - * - * If there's an active instance, then it 'might' be publishable - */ - public function canPublish() { - // Override any default behaviour, to allow queuedjobs to complete - if($this->isPublishJobRunning()) { - return true; - } - - if ($active = $this->getWorkflowInstance()) { - $publish = $active->canPublishTarget($this->owner); +class WorkflowApplicable extends DataExtension +{ + private static $has_one = array( + 'WorkflowDefinition' => WorkflowDefinition::class, + ); + + private static $many_many = array( + 'AdditionalWorkflowDefinitions' => WorkflowDefinition::class + ); + + private static $dependencies = array( + 'workflowService' => '%$' . WorkflowService::class, + ); + + /** + * + * Used to flag to this extension if there's a WorkflowPublishTargetJob running. + * @var boolean + */ + public $isPublishJobRunning = false; + + /** + * + * @param boolean $truth + */ + public function setIsPublishJobRunning($truth) + { + $this->isPublishJobRunning = $truth; + } + + /** + * + * @return boolean + */ + public function getIsPublishJobRunning() + { + return $this->isPublishJobRunning; + } + + /** + * + * @see {@link $this->isPublishJobRunning} + * @return boolean + */ + public function isPublishJobRunning() + { + $propIsSet = $this->getIsPublishJobRunning() ? true : false; + return class_exists(AbstractQueuedJob::class) && $propIsSet; + } + + /** + * @var WorkflowService + */ + public $workflowService; + + /** + * + * A cache var for the current workflow instance + * + * @var WorkflowInstance + */ + protected $currentInstance; + + public function updateSettingsFields(FieldList $fields) + { + $this->updateFields($fields); + } + + public function updateCMSFields(FieldList $fields) + { + if (!$this->owner->hasMethod('getSettingsFields')) { + $this->updateFields($fields); + } + + // Instantiate a hidden form field to pass the triggered workflow definition through, allowing a dynamic form action. + + $fields->push(HiddenField::create( + 'TriggeredWorkflowID' + )); + } + + public function updateFields(FieldList $fields) + { + if (!$this->owner->ID) { + return $fields; + } + + $tab = $fields->fieldByName('Root') ? $fields->findOrMakeTab('Root.Workflow') : $fields; + + if (Permission::check('APPLY_WORKFLOW')) { + $definition = new DropdownField('WorkflowDefinitionID', _t('WorkflowApplicable.DEFINITION', 'Applied Workflow')); + $definitions = $this->workflowService->getDefinitions()->map()->toArray(); + $definition->setSource($definitions); + $definition->setEmptyString(_t('WorkflowApplicable.INHERIT', 'Inherit from parent')); + $tab->push($definition); + + // Allow an optional selection of additional workflow definitions. + + if ($this->owner->WorkflowDefinitionID) { + $fields->removeByName('AdditionalWorkflowDefinitions'); + unset($definitions[$this->owner->WorkflowDefinitionID]); + $tab->push($additional = ListboxField::create( + 'AdditionalWorkflowDefinitions', + _t('WorkflowApplicable.ADDITIONAL_WORKFLOW_DEFINITIONS', 'Additional Workflows') + )); + $additional->setSource($definitions); + } + } + + // Display the effective workflow definition. + + if ($effective = $this->getWorkflowInstance()) { + $title = $effective->Definition()->Title; + $tab->push(ReadonlyField::create( + 'EffectiveWorkflow', + _t('WorkflowApplicable.EFFECTIVE_WORKFLOW', 'Effective Workflow'), + $title + )); + } + + if ($this->owner->ID) { + $config = new GridFieldConfig_Base(); + $config->addComponent(new GridFieldEditButton()); + $config->addComponent(new GridFieldDetailForm()); + + $insts = $this->owner->WorkflowInstances(); + $log = new GridField('WorkflowLog', _t('WorkflowApplicable.WORKFLOWLOG', 'Workflow Log'), $insts, $config); + + $tab->push($log); + } + } + + public function updateCMSActions(FieldList $actions) + { + $active = $this->workflowService->getWorkflowFor($this->owner); + $c = Controller::curr(); + if ($c && $c->hasExtension(AdvancedWorkflowExtension::class)) { + if ($active) { + if ($this->canEditWorkflow()) { + $workflowOptions = new Tab( + 'WorkflowOptions', + _t('SiteTree.WorkflowOptions', 'Workflow options', 'Expands a view for workflow specific buttons') + ); + + $menu = $actions->fieldByName('ActionMenus'); + if (!$menu) { + // create the menu for adding to any arbitrary non-sitetree object + $menu = $this->createActionMenu(); + $actions->push($menu); + } + + if (!$actions->fieldByName('ActionMenus.WorkflowOptions')) { + $menu->push($workflowOptions); + } + + $transitions = $active->CurrentAction()->getValidTransitions(); + + foreach ($transitions as $transition) { + if ($transition->canExecute($active)) { + $action = FormAction::create('updateworkflow-' . $transition->ID, $transition->Title) + ->setAttribute('data-transitionid', $transition->ID); + $workflowOptions->push($action); + } + } + + // $action = FormAction::create('updateworkflow', $active->CurrentAction() ? $active->CurrentAction()->Title : _t('WorkflowApplicable.UPDATE_WORKFLOW', 'Update Workflow')) + // ->setAttribute('data-icon', 'navigation'); + // $actions->fieldByName('MajorActions') ? $actions->fieldByName('MajorActions')->push($action) : $actions->push($action); + } + } else { + // Instantiate the workflow definition initial actions. + $definitions = $this->workflowService->getDefinitionsFor($this->owner); + if ($definitions) { + $menu = $actions->fieldByName('ActionMenus'); + if (is_null($menu)) { + // Instantiate a new action menu for any data objects. + + $menu = $this->createActionMenu(); + $actions->push($menu); + } + $tab = Tab::create( + 'AdditionalWorkflows' + ); + $addedFirst = false; + foreach ($definitions as $definition) { + if ($definition->getInitialAction() && $this->owner->canEdit()) { + $action = FormAction::create( + "startworkflow-{$definition->ID}", + $definition->InitialActionButtonText ? $definition->InitialActionButtonText : $definition->getInitialAction()->Title + )->addExtraClass('start-workflow')->setAttribute('data-workflow', $definition->ID); + + // The first element is the main workflow definition, and will be displayed as a major action. + if (!$addedFirst) { + $addedFirst = true; + $action->setAttribute('data-icon', 'navigation'); + $majorActions = $actions->fieldByName('MajorActions'); + $majorActions ? $majorActions->push($action) : $actions->push($action); + } else { + $tab->push($action); + } + } + } + // Only display menu if actions pushed to it + if ($tab->Fields()->exists()) { + $menu->insertBefore($tab, 'MoreOptions'); + } + } + } + } + } + + protected function createActionMenu() + { + $rootTabSet = new TabSet('ActionMenus'); + $rootTabSet->addExtraClass('ss-ui-action-tabset action-menus'); + return $rootTabSet; + } + + /** + * Included in CMS-generated email templates for a NotifyUsersWorkflowAction. + * Returns an absolute link to the CMS UI for a Page object + * + * @return string|null + */ + public function AbsoluteEditLink() + { + $CMSEditLink = null; + + if ($this->owner instanceof CMSPreviewable) { + $CMSEditLink = $this->owner->CMSEditLink(); + } elseif ($this->owner->hasMethod('WorkflowLink')) { + $CMSEditLink = $this->owner->WorkflowLink(); + } + + if ($CMSEditLink === null) { + return null; + } + + return Controller::join_links(Director::absoluteBaseURL(), $CMSEditLink); + } + + /** + * Included in CMS-generated email templates for a NotifyUsersWorkflowAction. + * Allows users to select a link in an email for direct access to the transition-selection dropdown in the CMS UI. + * + * @return string + */ + public function LinkToPendingItems() + { + $urlBase = Director::absoluteBaseURL(); + $urlFrag = 'admin/workflows/WorkflowDefinition/EditForm/field'; + $urlInst = $this->getWorkflowInstance(); + return Controller::join_links($urlBase, $urlFrag, 'PendingObjects', 'item', $urlInst->ID, 'edit'); + } + + /** + * After a workflow item is written, we notify the + * workflow so that it can take action if needbe + */ + public function onAfterWrite() + { + $instance = $this->getWorkflowInstance(); + if ($instance && $instance->CurrentActionID) { + $action = $instance->CurrentAction()->BaseAction()->targetUpdated($instance); + } + } + + public function WorkflowInstances() + { + return WorkflowInstance::get()->filter(array( + 'TargetClass' => $this->ownerBaseClass, + 'TargetID' => $this->owner->ID + )); + } + + /** + * Gets the current instance of workflow + * + * @return WorkflowInstance + */ + public function getWorkflowInstance() + { + if (!$this->currentInstance) { + $this->currentInstance = $this->workflowService->getWorkflowFor($this->owner); + } + + return $this->currentInstance; + } + + + /** + * Gets the history of a workflow instance + * + * @return DataObjectSet + */ + public function getWorkflowHistory($limit = null) + { + return $this->workflowService->getWorkflowHistoryFor($this->owner, $limit); + } + + /** + * Check all recent WorkflowActionIntances and return the most recent one with a Comment + * + * @param int $limit + * @return WorkflowActionInstance|null + */ + public function RecentWorkflowComment($limit = 10) + { + if ($actions = $this->getWorkflowHistory($limit)) { + foreach ($actions as $action) { + if ($action->Comment != '') { + return $action; + } + } + } + } + + /** + * Content can never be directly publishable if there's a workflow applied. + * + * If there's an active instance, then it 'might' be publishable + */ + public function canPublish() + { + // Override any default behaviour, to allow queuedjobs to complete + if ($this->isPublishJobRunning()) { + return true; + } + + if ($active = $this->getWorkflowInstance()) { + $publish = $active->canPublishTarget($this->owner); if (!is_null($publish)) { return $publish; } - } + } // use definition to determine if publishing directly is allowed $definition = $this->workflowService->getDefinitionFor($this->owner); if ($definition) { - if (!Member::currentUserID()) { + if (!Security::getCurrentUser()) { return false; } - $member = Member::currentUser(); + $member = Security::getCurrentUser(); $canPublish = $definition->canWorkflowPublish($member, $this->owner); return $canPublish; } - } - - /** - * Can only edit content that's NOT in another person's content changeset - */ - public function canEdit($member) { - // Override any default behaviour, to allow queuedjobs to complete - if($this->isPublishJobRunning()) { - return true; - } - - if ($active = $this->getWorkflowInstance()) { - return $active->canEditTarget($this->owner); - } - } - - /** - * Can a user edit the current workflow attached to this item? - */ - public function canEditWorkflow() { - $active = $this->getWorkflowInstance(); - if ($active) { - return $active->canEdit(); - } - return false; - } + } + + /** + * Can only edit content that's NOT in another person's content changeset + */ + public function canEdit($member) + { + // Override any default behaviour, to allow queuedjobs to complete + if ($this->isPublishJobRunning()) { + return true; + } + + if ($active = $this->getWorkflowInstance()) { + return $active->canEditTarget($this->owner); + } + } + + /** + * Can a user edit the current workflow attached to this item? + */ + public function canEditWorkflow() + { + $active = $this->getWorkflowInstance(); + if ($active) { + return $active->canEdit(); + } + return false; + } } diff --git a/code/extensions/WorkflowEmbargoExpiryExtension.php b/code/extensions/WorkflowEmbargoExpiryExtension.php index 3600149d..3397be2a 100644 --- a/code/extensions/WorkflowEmbargoExpiryExtension.php +++ b/code/extensions/WorkflowEmbargoExpiryExtension.php @@ -1,17 +1,31 @@ 'DBDatetime', - 'DesiredUnPublishDate' => 'DBDatetime', - 'PublishOnDate' => 'DBDatetime', - 'UnPublishOnDate' => 'DBDatetime', - 'AllowEmbargoedEditing' => 'Boolean', - ); +class WorkflowEmbargoExpiryExtension extends DataExtension +{ + private static $db = array( + 'DesiredPublishDate' => 'DBDatetime', + 'DesiredUnPublishDate' => 'DBDatetime', + 'PublishOnDate' => 'DBDatetime', + 'UnPublishOnDate' => 'DBDatetime', + 'AllowEmbargoedEditing' => 'Boolean', + ); - private static $has_one = array( - 'PublishJob' => 'QueuedJobDescriptor', - 'UnPublishJob' => 'QueuedJobDescriptor', - ); + private static $has_one = array( + 'PublishJob' => QueuedJobDescriptor::class, + 'UnPublishJob' => QueuedJobDescriptor::class, + ); - private static $dependencies = array( - 'workflowService' => '%$WorkflowService', - ); + private static $dependencies = array( + 'workflowService' => '%$' . WorkflowService::class, + ); private static $defaults = array( 'AllowEmbargoedEditing' => true ); - // This "config" option, might better be handled in _config - public static $showTimePicker = true; + // This "config" option, might better be handled in _config + public static $showTimePicker = true; - /** - * @var WorkflowService - */ - public $workflowService; + /** + * @var WorkflowService + */ + protected $workflowService; - /** - * Is a workflow in effect? - * - * @var bool - */ - public $isWorkflowInEffect = false; + /** + * Is a workflow in effect? + * + * @var bool + */ + public $isWorkflowInEffect = false; - /** - * A basic extended validation routine method return format - * - * @var array - */ - public static $extendedMethodReturn = array( - 'fieldName' =>null, - 'fieldField'=>null, - 'fieldMsg' =>null, - 'fieldValid'=>true - ); - - /** - * @param FieldList $fields - */ - public function updateCMSFields(FieldList $fields) { - - // requirements - // ------------ - - Requirements::add_i18n_javascript(ADVANCED_WORKFLOW_DIR . '/javascript/lang'); - - // Add timepicker functionality - // @see https://github.com/trentrichardson/jQuery-Timepicker-Addon - Requirements::css( - ADVANCED_WORKFLOW_DIR . '/thirdparty/javascript/jquery-ui/timepicker/jquery-ui-timepicker-addon.css' - ); - Requirements::css(ADVANCED_WORKFLOW_DIR . '/css/WorkflowCMS.css'); - Requirements::javascript( - ADVANCED_WORKFLOW_DIR . '/thirdparty/javascript/jquery-ui/timepicker/jquery-ui-sliderAccess.js' - ); - Requirements::javascript( - ADVANCED_WORKFLOW_DIR . '/thirdparty/javascript/jquery-ui/timepicker/jquery-ui-timepicker-addon.js' - ); - Requirements::javascript(ADVANCED_WORKFLOW_DIR . '/javascript/WorkflowField.js'); + /** + * A basic extended validation routine method return format + * + * @var array + */ + public static $extendedMethodReturn = array( + 'fieldName' => null, + 'fieldField' => null, + 'fieldMsg' => null, + 'fieldValid' => true, + ); + + /** + * @param FieldList $fields + */ + public function updateCMSFields(FieldList $fields) + { + // requirements + // ------------ + + $module = ModuleLoader::getModule('symbiote/silverstripe-advancedworkflow'); + + Requirements::add_i18n_javascript($module->getRelativeResourcePath('javascript/lang')); + + // Add timepicker functionality + // @see https://github.com/trentrichardson/jQuery-Timepicker-Addon + Requirements::css( + $module->getRelativeResourcePath('thirdparty/javascript/jquery-ui/timepicker/jquery-ui-timepicker-addon.css') + ); + Requirements::css($module->getRelativeResourcePath('css/WorkflowCMS.css')); + Requirements::javascript( + $module->getRelativeResourcePath('thirdparty/javascript/jquery-ui/timepicker/jquery-ui-sliderAccess.js') + ); + Requirements::javascript( + $module->getRelativeResourcePath('thirdparty/javascript/jquery-ui/timepicker/jquery-ui-timepicker-addon.js') + ); + Requirements::javascript($module->getRelativeResourcePath('javascript/WorkflowField.js')); // Fields // ------ - // we never show these explicitly in admin - $fields->removeByName('PublishJobID'); - $fields->removeByName('UnPublishJobID'); - - $this->setIsWorkflowInEffect(); - - $fields->findOrMakeTab( - 'Root.PublishingSchedule', - _t('WorkflowEmbargoExpiryExtension.TabTitle', 'Publishing Schedule') - ); - if ($this->getIsWorkflowInEffect()) { - - // add fields we want in this context - $fields->addFieldsToTab('Root.PublishingSchedule', array( - HeaderField::create( - 'PublishDateHeader', - _t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_H3', 'Expiry and Embargo'), - 3 - ), - LiteralField::create('PublishDateIntro', $this->getIntroMessage('PublishDateIntro')), - $dt = Datetimefield::create( - 'DesiredPublishDate', - _t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE', 'Requested publish date') - )->setRightTitle( + // we never show these explicitly in admin + $fields->removeByName('PublishJobID'); + $fields->removeByName('UnPublishJobID'); + + $this->setIsWorkflowInEffect(); + + $fields->findOrMakeTab( + 'Root.PublishingSchedule', + _t('WorkflowEmbargoExpiryExtension.TabTitle', 'Publishing Schedule') + ); + if ($this->getIsWorkflowInEffect()) { + // add fields we want in this context + $fields->addFieldsToTab('Root.PublishingSchedule', array( + HeaderField::create( + 'PublishDateHeader', + _t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_H3', 'Expiry and Embargo'), + 3 + ), + LiteralField::create('PublishDateIntro', $this->getIntroMessage('PublishDateIntro')), + $dt = DatetimeField::create( + 'DesiredPublishDate', + _t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE', 'Requested publish date') + )->setRightTitle( _t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_RIGHT_TITLE', 'To request this page to be published immediately leave the date and time fields blank') ), - $ut = Datetimefield::create( - 'DesiredUnPublishDate', - _t('WorkflowEmbargoExpiryExtension.REQUESTED_UNPUBLISH_DATE', 'Requested un-publish date') - )->setRightTitle( + $ut = DatetimeField::create( + 'DesiredUnPublishDate', + _t('WorkflowEmbargoExpiryExtension.REQUESTED_UNPUBLISH_DATE', 'Requested un-publish date') + )->setRightTitle( _t('WorkflowEmbargoExpiryExtension.REQUESTED_UNPUBLISH_DATE_RIGHT_TITLE', 'To request this page to never expire leave the date and time fields blank') ), - Datetimefield::create( - 'PublishOnDate', - _t('WorkflowEmbargoExpiryExtension.PUBLISH_ON', 'Scheduled publish date') - )->setDisabled(true), - Datetimefield::create( - 'UnPublishOnDate', - _t('WorkflowEmbargoExpiryExtension.UNPUBLISH_ON', 'Scheduled un-publish date') - )->setDisabled(true) - )); - } else { - - // remove fields that have been automatically added that we don't want - $fields->removeByName('DesiredPublishDate'); - $fields->removeByName('DesiredUnPublishDate'); - - // add fields we want in this context - $fields->addFieldsToTab('Root.PublishingSchedule', array( - HeaderField::create( - 'PublishDateHeader', - _t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_H3', 'Expiry and Embargo'), - 3 - ), - LiteralField::create('PublishDateIntro', $this->getIntroMessage('PublishDateIntro')), - $dt = Datetimefield::create( - 'PublishOnDate', - _t('WorkflowEmbargoExpiryExtension.PUBLISH_ON', 'Scheduled publish date') - ), - $ut = Datetimefield::create( - 'UnPublishOnDate', - _t('WorkflowEmbargoExpiryExtension.UNPUBLISH_ON', 'Scheduled un-publish date') - ), - )); - } - - $dt->getDateField()->setConfig('showcalendar', true); - $ut->getDateField()->setConfig('showcalendar', true); - $dt->getTimeField()->setConfig('timeformat', 'HH:mm:ss'); - $ut->getTimeField()->setConfig('timeformat', 'HH:mm:ss'); - - // Enable a jQuery-UI timepicker widget - if(self::$showTimePicker) { - $dt->getTimeField()->addExtraClass('hasTimePicker'); - $ut->getTimeField()->addExtraClass('hasTimePicker'); - } - } - - /** - * Clears any existing publish job against this dataobject - */ - public function clearPublishJob() { - $job = $this->owner->PublishJob(); - if($job && $job->exists()) { - $job->delete(); - } - $this->owner->PublishJobID = 0; - } - - /** - * Clears any existing unpublish job - */ - public function clearUnPublishJob() { - // Cancel any in-progress unpublish job - $job = $this->owner->UnPublishJob(); - if ($job && $job->exists()) { - $job->delete(); - } - $this->owner->UnPublishJobID = 0; - } - - /** - * Ensure the existence of a publish job at the specified time - * - * @param int $when Timestamp to start this job, or null to start immediately - */ - protected function ensurePublishJob($when) { - // Check if there is a prior job - if($this->owner->PublishJobID) { - $job = $this->owner->PublishJob(); - // Use timestamp for sake of comparison. - if($job && $job->exists() && strtotime($job->StartAfter) == $when) { - return; - } - $this->clearPublishJob(); - } - - // Create a new job with the specified schedule - $job = new WorkflowPublishTargetJob($this->owner, 'publish'); - $this->owner->PublishJobID = Injector::inst()->get('QueuedJobService') - ->queueJob($job, $when ? date('Y-m-d H:i:s', $when) : null); - } - - /** - * Ensure the existence of an unpublish job at the specified time - * - * @param int $when Timestamp to start this job, or null to start immediately - */ - protected function ensureUnPublishJob($when) { - // Check if there is a prior job - if($this->owner->UnPublishJobID) { - $job = $this->owner->UnPublishJob(); - // Use timestamp for sake of comparison. - if($job && $job->exists() && strtotime($job->StartAfter) == $when) { - return; - } - $this->clearUnPublishJob(); - } - - // Create a new job with the specified schedule - $job = new WorkflowPublishTargetJob($this->owner, 'unpublish'); - $this->owner->UnPublishJobID = Injector::inst()->get('QueuedJobService') - ->queueJob($job, $when ? date('Y-m-d H:i:s', $when) : null); - } - - public function onBeforeDuplicate($original, $doWrite) { + DatetimeField::create( + 'PublishOnDate', + _t('WorkflowEmbargoExpiryExtension.PUBLISH_ON', 'Scheduled publish date') + )->setDisabled(true), + DatetimeField::create( + 'UnPublishOnDate', + _t('WorkflowEmbargoExpiryExtension.UNPUBLISH_ON', 'Scheduled un-publish date') + )->setDisabled(true) + )); + } else { + // remove fields that have been automatically added that we don't want + $fields->removeByName('DesiredPublishDate'); + $fields->removeByName('DesiredUnPublishDate'); + + // add fields we want in this context + $fields->addFieldsToTab('Root.PublishingSchedule', array( + HeaderField::create( + 'PublishDateHeader', + _t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_H3', 'Expiry and Embargo'), + 3 + ), + LiteralField::create('PublishDateIntro', $this->getIntroMessage('PublishDateIntro')), + $dt = DatetimeField::create( + 'PublishOnDate', + _t('WorkflowEmbargoExpiryExtension.PUBLISH_ON', 'Scheduled publish date') + ), + $ut = DatetimeField::create( + 'UnPublishOnDate', + _t('WorkflowEmbargoExpiryExtension.UNPUBLISH_ON', 'Scheduled un-publish date') + ), + )); + } + + // Enable a jQuery-UI timepicker widget + // @todo re-validate this with new DatetimeField API + if (self::$showTimePicker) { + $dt->addExtraClass('hasTimePicker'); + $ut->addExtraClass('hasTimePicker'); + } + } + + /** + * Clears any existing publish job against this dataobject + */ + public function clearPublishJob() + { + $job = $this->owner->PublishJob(); + if ($job && $job->exists()) { + $job->delete(); + } + $this->owner->PublishJobID = 0; + } + + /** + * Clears any existing unpublish job + */ + public function clearUnPublishJob() + { + // Cancel any in-progress unpublish job + $job = $this->owner->UnPublishJob(); + if ($job && $job->exists()) { + $job->delete(); + } + $this->owner->UnPublishJobID = 0; + } + + /** + * Ensure the existence of a publish job at the specified time + * + * @param int $when Timestamp to start this job, or null to start immediately + */ + protected function ensurePublishJob($when) + { + // Check if there is a prior job + if ($this->owner->PublishJobID) { + $job = $this->owner->PublishJob(); + // Use timestamp for sake of comparison. + if ($job && $job->exists() && DBDatetime::create()->setValue($job->StartAfter)->getTimestamp() == $when) { + return; + } + $this->clearPublishJob(); + } + + // Create a new job with the specified schedule + $job = new WorkflowPublishTargetJob($this->owner, 'publish'); + $this->owner->PublishJobID = Injector::inst()->get(QueuedJobService::class) + ->queueJob($job, $when ? date('Y-m-d H:i:s', $when) : null); + } + + /** + * Ensure the existence of an unpublish job at the specified time + * + * @param int $when Timestamp to start this job, or null to start immediately + */ + protected function ensureUnPublishJob($when) + { + // Check if there is a prior job + if ($this->owner->UnPublishJobID) { + $job = $this->owner->UnPublishJob(); + // Use timestamp for sake of comparison. + if ($job && $job->exists() && DBDatetime::create()->setValue($job->StartAfter)->getTimestamp() == $when) { + return; + } + $this->clearUnPublishJob(); + } + + // Create a new job with the specified schedule + $job = new WorkflowPublishTargetJob($this->owner, 'unpublish'); + $this->owner->UnPublishJobID = Injector::inst()->get(QueuedJobService::class) + ->queueJob($job, $when ? date('Y-m-d H:i:s', $when) : null); + } + + public function onBeforeDuplicate($original, $doWrite) + { $clone = $this->owner; $clone->PublishOnDate = null; @@ -252,22 +267,23 @@ public function onBeforeDuplicate($original, $doWrite) { $clone->clearUnPublishJob(); } - /** - * {@see PublishItemWorkflowAction} for approval of requested publish dates - */ - public function onBeforeWrite() { - parent::onBeforeWrite(); - - // only operate on staging content for this extension; otherwise, you - // need to publish the page to be able to set a 'future' publish... - // while the same could be said for the unpublish, the 'publish' state - // is the one that must be avoided so we allow setting the 'unpublish' - // date for as-yet-not-published content. - if (Versioned::get_stage() === Versioned::LIVE) { + /** + * {@see PublishItemWorkflowAction} for approval of requested publish dates + */ + public function onBeforeWrite() + { + parent::onBeforeWrite(); + + // only operate on staging content for this extension; otherwise, you + // need to publish the page to be able to set a 'future' publish... + // while the same could be said for the unpublish, the 'publish' state + // is the one that must be avoided so we allow setting the 'unpublish' + // date for as-yet-not-published content. + if (Versioned::get_stage() === Versioned::LIVE) { return; } - /* + /* * Without checking if there's actually a workflow in effect, simply saving * as draft, would clear the Scheduled Publish & Unpublish date fields, which we obviously * don't want during a workflow: These date fields should be treated as a content @@ -289,39 +305,41 @@ public function onBeforeWrite() { } } - // Jobs can only be queued for records that already exist - if(!$this->owner->ID) return; + // Jobs can only be queued for records that already exist + if (!$this->owner->ID) { + return; + } - // Check requested dates of publish / unpublish, and whether the page should have already been unpublished - $now = strtotime(DBDatetime::now()->getValue()); - $publishTime = strtotime($this->owner->PublishOnDate); - $unPublishTime = strtotime($this->owner->UnPublishOnDate); + // Check requested dates of publish / unpublish, and whether the page should have already been unpublished + $now = DBDatetime::now()->getTimestamp(); + $publishTime = $this->owner->dbObject('PublishOnDate')->getTimestamp(); + $unPublishTime = $this->owner->dbObject('UnPublishOnDate')->getTimestamp(); - // We should have a publish job if: - // if no unpublish or publish time, then the Workflow Publish Action will publish without a job - if((!$unPublishTime && $publishTime) // the unpublish date is not set + // We should have a publish job if: + // if no unpublish or publish time, then the Workflow Publish Action will publish without a job + if ((!$unPublishTime && $publishTime) // the unpublish date is not set || ( $unPublishTime > $now // unpublish date has not passed && ($publishTime && ($publishTime < $unPublishTime)) // publish date not set or happens before unpublish date ) - ) { - // Trigger time immediately if passed - $this->ensurePublishJob($publishTime < $now ? null : $publishTime); - } else { - $this->clearPublishJob(); - } - - // We should have an unpublish job if: - if($unPublishTime // we have an unpublish date + ) { + // Trigger time immediately if passed + $this->ensurePublishJob($publishTime < $now ? null : $publishTime); + } else { + $this->clearPublishJob(); + } + + // We should have an unpublish job if: + if ($unPublishTime // we have an unpublish date && $publishTime < $unPublishTime // publish date is before to unpublish date ) { - // Trigger time immediately if passed - $this->ensureUnPublishJob($unPublishTime < $now ? null : $unPublishTime); - } else { - $this->clearUnPublishJob(); - } - } + // Trigger time immediately if passed + $this->ensureUnPublishJob($unPublishTime < $now ? null : $unPublishTime); + } else { + $this->clearUnPublishJob(); + } + } /** * Add badges to the site tree view to show that a page has been scheduled for publishing or unpublishing @@ -340,27 +358,28 @@ public function updateStatusFlags(&$flags) if ($embargo && $expiry) { $flags['embargo_expiry'] = array( 'text' => _t('WorkflowEmbargoExpiryExtension.BADGE_PUBLISH_UNPUBLISH', 'Embargo+Expiry'), - 'title' => sprintf('%s: %s, %s: %s', + 'title' => sprintf( + '%s: %s, %s: %s', _t('WorkflowEmbargoExpiryExtension.PUBLISH_ON', 'Scheduled publish date'), $this->owner->PublishOnDate, _t('WorkflowEmbargoExpiryExtension.UNPUBLISH_ON', 'Scheduled un-publish date'), $this->owner->UnPublishOnDate ), ); - } - elseif ($embargo) { + } elseif ($embargo) { $flags['embargo'] = array( 'text' => _t('WorkflowEmbargoExpiryExtension.BADGE_PUBLISH', 'Embargo'), - 'title' => sprintf('%s: %s', + 'title' => sprintf( + '%s: %s', _t('WorkflowEmbargoExpiryExtension.PUBLISH_ON', 'Scheduled publish date'), $this->owner->PublishOnDate ), ); - } - elseif ($expiry) { + } elseif ($expiry) { $flags['expiry'] = array( 'text' => _t('WorkflowEmbargoExpiryExtension.BADGE_UNPUBLISH', 'Expiry'), - 'title' => sprintf('%s: %s', + 'title' => sprintf( + '%s: %s', _t('WorkflowEmbargoExpiryExtension.UNPUBLISH_ON', 'Scheduled un-publish date'), $this->owner->UnPublishOnDate ), @@ -368,48 +387,51 @@ public function updateStatusFlags(&$flags) } } - /* + /* * Define an array of message-parts for use by {@link getIntroMessage()} * * @param string $key * @return array */ - public function getIntroMessageParts($key) { - $parts = array( - 'PublishDateIntro' => array( - 'INTRO'=>_t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_INTRO','Enter a date and/or time to specify embargo and expiry dates.'), - 'BULLET_1'=>_t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_INTRO_BULLET_1','These settings won\'t take effect until any approval actions are run'), - 'BULLET_2'=>_t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_INTRO_BULLET_2','If an embargo is already set, adding a new one prior to that date\'s passing will overwrite it') - ) - ); - // If there's no effective workflow, no need for the first bullet-point - if(!$this->getIsWorkflowInEffect()) { - $parts['PublishDateIntro']['BULLET_1'] = false; - } - return $parts[$key]; - } - - /* + public function getIntroMessageParts($key) + { + $parts = array( + 'PublishDateIntro' => array( + 'INTRO'=>_t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_INTRO', 'Enter a date and/or time to specify embargo and expiry dates.'), + 'BULLET_1'=>_t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_INTRO_BULLET_1', 'These settings won\'t take effect until any approval actions are run'), + 'BULLET_2'=>_t('WorkflowEmbargoExpiryExtension.REQUESTED_PUBLISH_DATE_INTRO_BULLET_2', 'If an embargo is already set, adding a new one prior to that date\'s passing will overwrite it') + ) + ); + // If there's no effective workflow, no need for the first bullet-point + if (!$this->getIsWorkflowInEffect()) { + $parts['PublishDateIntro']['BULLET_1'] = false; + } + return $parts[$key]; + } + + /* * Display some messages to the user, a little more complex that a simple one-liner * * @param string $key * @return string */ - public function getIntroMessage($key) { - $msg = $this->getIntroMessageParts($key); - $curr = Controller::curr(); - $msg = $curr->customise($msg)->renderWith('Includes/embargoIntro'); - return $msg; - } - - /* + public function getIntroMessage($key) + { + $msg = $this->getIntroMessageParts($key); + $curr = Controller::curr(); + $msg = $curr->customise($msg)->renderWith('Includes/embargoIntro'); + return $msg; + } + + /* * Validate */ - public function getCMSValidator() { - $required = new AWRequiredFields(); - $required->setCaller($this); - return $required; - } + public function getCMSValidator() + { + $required = new AWRequiredFields(); + $required->setCaller($this); + return $required; + } /** * This is called in the AWRequiredFields class, this validates whether an Embargo and Expiry are not equal and that @@ -421,25 +443,25 @@ public function getCMSValidator() { public function extendedRequiredFieldsEmbargoExpiry($data) { $response = array( - 'fieldName' => 'DesiredUnPublishDate[date]', + 'fieldName' => 'DesiredUnPublishDate[date]', 'fieldField' => null, - 'fieldMsg' => null, + 'fieldMsg' => null, 'fieldValid' => true ); if (isset($data['DesiredPublishDate'], $data['DesiredUnPublishDate'])) { - $publish = strtotime($data['DesiredPublishDate']); - $unpublish = strtotime($data['DesiredUnPublishDate']); + $publish = DBDatetime::create()->setValue($data['DesiredPublishDate'])->getTimestamp(); + $unpublish = DBDatetime::create()->setValue($data['DesiredUnPublishDate'])->getTimestamp(); // the times are the same if ($publish && $unpublish && $publish == $unpublish) { $response = array_merge($response, array( - 'fieldMsg' => _t('WorkflowEmbargoExpiryExtension.INVALIDSAMEEMBARGOEXPIRY', 'The publish date and unpublish date cannot be the same.'), + 'fieldMsg' => _t('WorkflowEmbargoExpiryExtension.INVALIDSAMEEMBARGOEXPIRY', 'The publish date and unpublish date cannot be the same.'), 'fieldValid' => false )); } elseif ($publish && $unpublish && $publish > $unpublish) { $response = array_merge($response, array( - 'fieldMsg' => _t('WorkflowEmbargoExpiryExtension.INVALIDEXPIRY', 'The unpublish date cannot be before the publish date.'), + 'fieldMsg' => _t('WorkflowEmbargoExpiryExtension.INVALIDEXPIRY', 'The unpublish date cannot be before the publish date.'), 'fieldValid' => false )); } @@ -448,30 +470,33 @@ public function extendedRequiredFieldsEmbargoExpiry($data) return $response; } - /* - * Format a date according to member/user preferences - * - * @param string $date - * @return string $date - */ - public function getUserDate($date) { - $date = new Zend_Date($date); - $member = Member::currentUser(); - return $date->toString($member->getDateFormat().' '.$member->getTimeFormat()); - } + /** + * Format a date according to member/user preferences + * + * @param string $date + * @return string $date + */ + public function getUserDate($date) + { + $date = DBDatetime::create()->setValue($date); + $member = Security::getCurrentUser(); + return $date->FormatFromSettings($member); + } - /* + /* * Sets property as boolean true|false if an effective workflow is found or not */ - public function setIsWorkflowInEffect() { - // if there is a workflow applied, we can't set the publishing date directly, only the 'desired' publishing date - $effective = $this->workflowService->getDefinitionFor($this->owner); - $this->isWorkflowInEffect = $effective?true:false; - } + public function setIsWorkflowInEffect() + { + // if there is a workflow applied, we can't set the publishing date directly, only the 'desired' publishing date + $effective = $this->getWorkflowService()->getDefinitionFor($this->owner); + $this->isWorkflowInEffect = $effective ? true : false; + } - public function getIsWorkflowInEffect() { - return $this->isWorkflowInEffect; - } + public function getIsWorkflowInEffect() + { + return $this->isWorkflowInEffect; + } /** * Returns whether a publishing date has been set and is after the current date @@ -483,8 +508,8 @@ public function getIsPublishScheduled() if (!$this->owner->PublishOnDate) { return false; } - $now = strtotime(DBDatetime::now()->getValue()); - $publish = strtotime($this->owner->PublishOnDate); + $now = DBDatetime::now()->getTimestamp(); + $publish = $this->owner->dbObject('PublishOnDate')->getTimestamp(); return $now < $publish; } @@ -499,8 +524,8 @@ public function getIsUnPublishScheduled() if (!$this->owner->UnPublishOnDate) { return false; } - $now = strtotime(DBDatetime::now()->getValue()); - $unpublish = strtotime($this->owner->UnPublishOnDate); + $now = DBDatetime::now()->getTimestamp(); + $unpublish = $this->owner->dbObject('UnPublishOnDate')->getTimestamp(); return $now < $unpublish; } @@ -509,22 +534,43 @@ public function getIsUnPublishScheduled() * Add edit check for when publishing has been scheduled and if any workflow definitions want the item to be * disabled. * - * @param $member - * @return bool + * @param Member $member + * @return bool|null */ - public function canEdit($member) { - if (!Permission::check('EDIT_EMBARGOED_WORKFLOW') && // not given global/override permission to edit - !$this->AllowEmbargoedEditing) { // item flagged as not editable - $now = strtotime(DBDatetime::now()->getValue()); - $publishTime = strtotime($this->owner->PublishOnDate); - - if ($publishTime && $publishTime > $now || // when scheduled publish date is in the future - // when there isn't a publish date, but a Job is in place (publish immediately, but queued jobs is waiting) - (!$publishTime && $this->owner->PublishJobID != 0) - ) { - return false; - } - } - } + public function canEdit($member) + { + if (!Permission::check('EDIT_EMBARGOED_WORKFLOW') && // not given global/override permission to edit + !$this->owner->AllowEmbargoedEditing) { // item flagged as not editable + $publishTime = $this->owner->dbObject('PublishOnDate'); + + if ($publishTime && $publishTime->InFuture() || // when scheduled publish date is in the future + // when there isn't a publish date, but a Job is in place (publish immediately, but queued jobs is waiting) + (!$publishTime && $this->owner->PublishJobID != 0) + ) { + return false; + } + } + } + /** + * Set the workflow service instance + * + * @param WorkflowService $workflowService + * @return $this + */ + public function setWorkflowService(WorkflowService $workflowService) + { + $this->workflowService = $workflowService; + return $this; + } + + /** + * Get the workflow service instance + * + * @return WorkflowService + */ + public function getWorkflowService() + { + return $this->workflowService; + } } diff --git a/code/formfields/WorkflowField.php b/code/formfields/WorkflowField.php index 67a4a8cb..883b63fc 100644 --- a/code/formfields/WorkflowField.php +++ b/code/formfields/WorkflowField.php @@ -1,8 +1,21 @@ definition = $definition; - $this->addExtraClass('workflow-field'); - - parent::__construct($name, $title); - } - - public function action() { - return new WorkflowFieldActionController($this, 'action'); - } - - public function transition() { - return new WorkflowFieldTransitionController($this, 'transition'); - } - - public function sort($request) { - if(!SecurityToken::inst()->checkRequest($request)) { - $this->httpError(404); - } - - $class = $request->postVar('class'); - $ids = $request->postVar('id'); - - if($class == 'WorkflowAction') { - $objects = $this->Definition()->Actions(); - } elseif($class == 'WorkflowTransition') { - $parent = $request->postVar('parent'); - $action = $this->Definition()->Actions()->byID($parent); - - if(!$action) { - $this->httpError(400, _t('AdvancedWorkflowAdmin.INVALIDPARENTID', 'An invalid parent ID was specified.')); - } - - $objects = $action->Transitions(); - } else { - $this->httpError(400, _t('AdvancedWorkflowAdmin.INVALIDCLASSTOORDER', 'An invalid class to order was specified.')); - } - - if(array_diff($ids, $objects->column('ID'))) { - $this->httpError(400, _t('AdvancedWorkflowAdmin.INVALIDIDLIST', 'An invalid list of IDs was provided.')); - } - - singleton('WorkflowService')->reorder($objects, $ids); - - return new SS_HTTPResponse( - null, 200, _t('AdvancedWorkflowAdmin.SORTORDERSAVED', 'The sort order has been saved.') - ); - } - - public function getTemplate() { - return 'WorkflowField'; - } - - public function FieldHolder($properties = array()) { - Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); - Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); - Requirements::javascript(ADVANCED_WORKFLOW_DIR . '/javascript/WorkflowField.js'); - Requirements::css(ADVANCED_WORKFLOW_DIR . '/css/WorkflowField.css'); - - return $this->Field($properties); - } - - public function Definition() { - return $this->definition; - } - - public function ActionLink() { - $parts = func_get_args(); - array_unshift($parts, 'action'); - - return $this->Link(implode('/', $parts)); - } - - public function TransitionLink() { - $parts = func_get_args(); - array_unshift($parts, 'transition'); - - return $this->Link(implode('/', $parts)); - } - - public function CreateableActions() { - $list = new ArrayList(); - $classes = ClassInfo::subclassesFor('WorkflowAction'); - - array_shift($classes); - sort($classes); - - foreach($classes as $class) { - $reflect = new ReflectionClass($class); - $can = singleton($class)->canCreate() && !$reflect->isAbstract(); - - if($can) $list->push(new ArrayData(array( - 'Title' => singleton($class)->singular_name(), - 'Class' => $class - ))); - } - - return $list; - } - +class WorkflowField extends FormField +{ + private static $allowed_actions = array( + 'action', + 'transition', + 'sort' + ); + + protected $definition; + + public function __construct($name, $title, WorkflowDefinition $definition) + { + $this->definition = $definition; + $this->addExtraClass('workflow-field'); + + parent::__construct($name, $title); + } + + public function action() + { + return new WorkflowFieldActionController($this, 'action'); + } + + public function transition() + { + return new WorkflowFieldTransitionController($this, 'transition'); + } + + public function sort($request) + { + if (!SecurityToken::inst()->checkRequest($request)) { + $this->httpError(404); + } + + $class = $request->postVar('class'); + $ids = $request->postVar('id'); + + if ($class == WorkflowAction::class) { + $objects = $this->Definition()->Actions(); + } elseif ($class == WorkflowTransition::class) { + $parent = $request->postVar('parent'); + $action = $this->Definition()->Actions()->byID($parent); + + if (!$action) { + $this->httpError(400, _t('AdvancedWorkflowAdmin.INVALIDPARENTID', 'An invalid parent ID was specified.')); + } + + $objects = $action->Transitions(); + } else { + $this->httpError(400, _t('AdvancedWorkflowAdmin.INVALIDCLASSTOORDER', 'An invalid class to order was specified.')); + } + + if (array_diff($ids, $objects->column('ID'))) { + $this->httpError(400, _t('AdvancedWorkflowAdmin.INVALIDIDLIST', 'An invalid list of IDs was provided.')); + } + + singleton(WorkflowService::class)->reorder($objects, $ids); + + return new HTTPResponse( + null, + 200, + _t('AdvancedWorkflowAdmin.SORTORDERSAVED', 'The sort order has been saved.') + ); + } + + public function getTemplate() + { + return __CLASS__; + } + + public function FieldHolder($properties = array()) + { + $workflow = ModuleLoader::getModule('symbiote/silverstripe-advancedworkflow'); + $admin = ModuleLoader::getModule('silverstripe/admin'); + + Requirements::javascript($admin->getRelativeResourcePath('thirdparty/jquery/jquery.js')); + Requirements::javascript($admin->getRelativeResourcePath('thirdparty/jquery-entwine/dist/jquery.entwine-dist.js')); + Requirements::javascript($workflow->getRelativeResourcePath('javascript/WorkflowField.js')); + Requirements::css($workflow->getRelativeResourcePath('css/WorkflowField.css')); + + return $this->Field($properties); + } + + public function Definition() + { + return $this->definition; + } + + public function ActionLink() + { + $parts = func_get_args(); + array_unshift($parts, 'action'); + + return $this->Link(implode('/', $parts)); + } + + public function TransitionLink() + { + $parts = func_get_args(); + array_unshift($parts, 'transition'); + + return $this->Link(implode('/', $parts)); + } + + public function CreateableActions() + { + $list = new ArrayList(); + $classes = ClassInfo::subclassesFor(WorkflowAction::class); + + array_shift($classes); + sort($classes); + + foreach ($classes as $class) { + $reflect = new ReflectionClass($class); + $can = singleton($class)->canCreate() && !$reflect->isAbstract(); + + if ($can) { + $list->push(new ArrayData(array( + 'Title' => singleton($class)->singular_name(), + 'Class' => $class + ))); + } + } + + return $list; + } } diff --git a/code/formfields/WorkflowFieldActionController.php b/code/formfields/WorkflowFieldActionController.php index 69dfdcd9..13ecaccf 100644 --- a/code/formfields/WorkflowFieldActionController.php +++ b/code/formfields/WorkflowFieldActionController.php @@ -1,75 +1,84 @@ 'handleAdd', - 'item/$ID' => 'handleItem' - ); - - private static $allowed_actions = array( - 'handleAdd', - 'handleItem' - ); - - protected $parent; - protected $name; - - public function __construct($parent, $name) { - $this->parent = $parent; - $this->name = $name; - - parent::__construct(); - } - - public function handleAdd() { - $class = $this->request->param('Class'); - - if(!class_exists($class) || !is_subclass_of($class, 'WorkflowAction')) { - $this->httpError(400); - } - - $reflector = new ReflectionClass($class); - - if($reflector->isAbstract() || !singleton($class)->canCreate()) { - $this->httpError(400); - } - - $record = new $class(); - $record->WorkflowDefID = $this->parent->Definition()->ID; - - return new WorkflowFieldItemController($this, "new/$class", $record); - } - - public function handleItem() { - $id = $this->request->param('ID'); - $defn = $this->parent->Definition(); - $action = $defn->Actions()->byID($id); - - if(!$action) { - $this->httpError(404); - } - - if(!$action->canEdit()) { - $this->httpError(403); - } - - return new WorkflowFieldItemController($this, "item/$id", $action); - } - - public function RootField() { - return $this->parent; - } - - public function Link($action = null) { - return Controller::join_links($this->parent->Link(), $this->name, $action); - } - +class WorkflowFieldActionController extends RequestHandler +{ + private static $url_handlers = array( + 'new/$Class' => 'handleAdd', + 'item/$ID' => 'handleItem' + ); + + private static $allowed_actions = array( + 'handleAdd', + 'handleItem' + ); + + protected $parent; + protected $name; + + public function __construct($parent, $name) + { + $this->parent = $parent; + $this->name = $name; + + parent::__construct(); + } + + public function handleAdd() + { + $class = $this->request->param('Class'); + + if (!class_exists($class) || !is_subclass_of($class, WorkflowAction::class)) { + $this->httpError(400); + } + + $reflector = new ReflectionClass($class); + + if ($reflector->isAbstract() || !singleton($class)->canCreate()) { + $this->httpError(400); + } + + $record = new $class(); + $record->WorkflowDefID = $this->parent->Definition()->ID; + + return new WorkflowFieldItemController($this, "new/$class", $record); + } + + public function handleItem() + { + $id = $this->request->param('ID'); + $defn = $this->parent->Definition(); + $action = $defn->Actions()->byID($id); + + if (!$action) { + $this->httpError(404); + } + + if (!$action->canEdit()) { + $this->httpError(403); + } + + return new WorkflowFieldItemController($this, "item/$id", $action); + } + + public function RootField() + { + return $this->parent; + } + + public function Link($action = null) + { + return Controller::join_links($this->parent->Link(), $this->name, $action); + } } diff --git a/code/formfields/WorkflowFieldItemController.php b/code/formfields/WorkflowFieldItemController.php index 95ead3ea..9c56c317 100644 --- a/code/formfields/WorkflowFieldItemController.php +++ b/code/formfields/WorkflowFieldItemController.php @@ -1,103 +1,116 @@ parent = $parent; - $this->name = $name; - $this->record = $record; - - parent::__construct(); - } - - public function index() { - return $this->edit(); - } - - public function edit() { - return $this->Form()->forTemplate(); - } - - public function Form() { - $record = $this->record; - $fields = $record->getCMSFields(); - $validator = $record->hasMethod('getValidator') ? $record->getValidator() : null; - - $save = FormAction::create('doSave', _t('WorkflowReminderTask.SAVE', 'Save')); - $save->addExtraClass('ss-ui-button ss-ui-action-constructive') - ->setAttribute('data-icon', 'accept') - ->setUseButtonTag(true); - - $form = new Form($this, 'Form', $fields, new FieldList($save), $validator); - if($record && $record instanceof DataObject && $record->exists()){ - $form->loadDataFrom($record); - } - return $form; - } - - public function doSave($data, $form) { - $record = $form->getRecord(); - - if(!$record || !$record->exists()){ - $record = $this->record; - } - - if(!$record->canEdit()) { - $this->httpError(403); - } - - if(!$record->isInDb()) { - $record->write(); - } - - $form->saveInto($record); - $record->write(); - - return $this->RootField()->forTemplate(); - } - - public function delete($request) { - if(!SecurityToken::inst()->checkRequest($request)) { - $this->httpError(400); - } - - if(!$request->isPOST()) { - $this->httpError(400); - } - - if(!$this->record->canDelete()) { - $this->httpError(403); - } - - $this->record->delete(); - return $this->RootField()->forTemplate(); - } - - public function RootField() { - return $this->parent->RootField(); - } - - public function Link($action = null) { - return Controller::join_links($this->parent->Link(), $this->name, $action); - } - +class WorkflowFieldItemController extends Controller +{ + private static $allowed_actions = array( + 'index', + 'edit', + 'delete', + 'Form' + ); + + protected $parent; + protected $name; + + public function __construct($parent, $name, $record) + { + $this->parent = $parent; + $this->name = $name; + $this->record = $record; + + parent::__construct(); + } + + public function index() + { + return $this->edit(); + } + + public function edit() + { + return $this->Form()->forTemplate(); + } + + public function Form() + { + $record = $this->record; + $fields = $record->getCMSFields(); + $validator = $record->hasMethod('getValidator') ? $record->getValidator() : null; + + $save = FormAction::create('doSave', _t('WorkflowReminderTask.SAVE', 'Save')); + $save->addExtraClass('ss-ui-button ss-ui-action-constructive') + ->setAttribute('data-icon', 'accept') + ->setUseButtonTag(true); + + /** @skipUpgrade */ + $form = Form::create($this, 'Form', $fields, FieldList::create($save), $validator); + if ($record && $record instanceof DataObject && $record->exists()) { + $form->loadDataFrom($record); + } + return $form; + } + + public function doSave($data, $form) + { + $record = $form->getRecord(); + + if (!$record || !$record->exists()) { + $record = $this->record; + } + + if (!$record->canEdit()) { + $this->httpError(403); + } + + if (!$record->isInDb()) { + $record->write(); + } + + $form->saveInto($record); + $record->write(); + + return $this->RootField()->forTemplate(); + } + + public function delete($request) + { + if (!SecurityToken::inst()->checkRequest($request)) { + $this->httpError(400); + } + + if (!$request->isPOST()) { + $this->httpError(400); + } + + if (!$this->record->canDelete()) { + $this->httpError(403); + } + + $this->record->delete(); + return $this->RootField()->forTemplate(); + } + + public function RootField() + { + return $this->parent->RootField(); + } + + public function Link($action = null) + { + return Controller::join_links($this->parent->Link(), $this->name, $action); + } } diff --git a/code/formfields/WorkflowFieldTransitionController.php b/code/formfields/WorkflowFieldTransitionController.php index a51e7c58..7e38713a 100644 --- a/code/formfields/WorkflowFieldTransitionController.php +++ b/code/formfields/WorkflowFieldTransitionController.php @@ -1,73 +1,82 @@ 'handleAdd', - 'item/$ID!' => 'handleItem' - ); - - private static $allowed_actions = array( - 'handleAdd', - 'handleItem' - ); - - protected $parent; - protected $name; - - public function __construct($parent, $name) { - $this->parent = $parent; - $this->name = $name; - - parent::__construct(); - } - - public function handleAdd() { - $parent = $this->request->param('ParentID'); - $action = WorkflowAction::get()->byID($this->request->param('ParentID')); - - if(!$action || $action->WorkflowDefID != $this->RootField()->Definition()->ID) { - $this->httpError(404); - } - - if(!singleton('WorkflowTransition')->canCreate()) { - $this->httpError(403); - } - - $transition = new WorkflowTransition(); - $transition->ActionID = $action->ID; - - return new WorkflowFieldItemController($this, "new/$parent", $transition); - } - - public function handleItem() { - $id = $this->request->param('ID'); - $trans = WorkflowTransition::get()->byID($id); - - if(!$trans || $trans->Action()->WorkflowDefID != $this->RootField()->Definition()->ID) { - $this->httpError(404); - } - - if(!$trans->canEdit()) { - $this->httpError(403); - } - - return new WorkflowFieldItemController($this, "item/$id", $trans); - } - - public function RootField() { - return $this->parent; - } - - public function Link($action = null) { - return Controller::join_links($this->parent->Link(), $this->name, $action); - } - +class WorkflowFieldTransitionController extends RequestHandler +{ + private static $url_handlers = array( + 'new/$ParentID!' => 'handleAdd', + 'item/$ID!' => 'handleItem' + ); + + private static $allowed_actions = array( + 'handleAdd', + 'handleItem' + ); + + protected $parent; + protected $name; + + public function __construct($parent, $name) + { + $this->parent = $parent; + $this->name = $name; + + parent::__construct(); + } + + public function handleAdd() + { + $parent = $this->request->param('ParentID'); + $action = WorkflowAction::get()->byID($this->request->param('ParentID')); + + if (!$action || $action->WorkflowDefID != $this->RootField()->Definition()->ID) { + $this->httpError(404); + } + + if (!singleton(WorkflowTransition::class)->canCreate()) { + $this->httpError(403); + } + + $transition = new WorkflowTransition(); + $transition->ActionID = $action->ID; + + return new WorkflowFieldItemController($this, "new/$parent", $transition); + } + + public function handleItem() + { + $id = $this->request->param('ID'); + $trans = WorkflowTransition::get()->byID($id); + + if (!$trans || $trans->Action()->WorkflowDefID != $this->RootField()->Definition()->ID) { + $this->httpError(404); + } + + if (!$trans->canEdit()) { + $this->httpError(403); + } + + return new WorkflowFieldItemController($this, "item/$id", $trans); + } + + public function RootField() + { + return $this->parent; + } + + public function Link($action = null) + { + return Controller::join_links($this->parent->Link(), $this->name, $action); + } } diff --git a/code/forms/AWRequiredFields.php b/code/forms/AWRequiredFields.php index 9d10f262..f1223a6e 100644 --- a/code/forms/AWRequiredFields.php +++ b/code/forms/AWRequiredFields.php @@ -1,5 +1,7 @@ setData($data); - public function php($data) { - $valid = parent::php($data); - $this->setData($data); + // Fetch any extended validation routines on the caller + $extended = $this->getExtendedValidationRoutines(); - // Fetch any extended validation routines on the caller - $extended = $this->getExtendedValidationRoutines(); + // Only deal-to extended routines once the parent is done + if ($valid && $extended['fieldValid'] !== true) { + $fieldName = $extended['fieldName']; + $formField = $extended['fieldField']; + $errorMessage = sprintf( + $extended['fieldMsg'], + strip_tags('"'.(($formField && $formField->Title()) ? $formField->Title() : $fieldName).'"') + ); - // Only deal-to extended routines once the parent is done - if($valid && $extended['fieldValid'] !== true) { - $fieldName = $extended['fieldName']; - $formField = $extended['fieldField']; - $errorMessage = sprintf($extended['fieldMsg'], - strip_tags('"'.(($formField && $formField->Title()) ? $formField->Title() : $fieldName).'"')); + if ($formField && $msg = $formField->getCustomValidationMessage()) { + $errorMessage = $msg; + } - if($formField && $msg = $formField->getCustomValidationMessage()) { - $errorMessage = $msg; - } - - $this->validationError( - $fieldName, - $errorMessage, - "required" - ); - $valid = false; - } - return $valid; - } + $this->validationError( + $fieldName, + $errorMessage, + "required" + ); + $valid = false; + } + return $valid; + } - /* - * Allows for the addition of an arbitrary no. additional, dedicated and "extended" validation methods on classes that call AWRequiredFields. - * To add specific validation methods to a caller: - * - * 1). Write each checking method using this naming prototype: public function extendedRequiredFieldsXXX(). All methods so named will be called. - * 2). Call AWRequiredFields->setCaller($this) - * - * Each extended method thus called, should return an array of a specific format. (See: static $extendedMethodReturn on the caller) - * - * @return array $return - */ - public function getExtendedValidationRoutines() { - // Setup a return array - $return = array( - 'fieldValid'=>true, - 'fieldName' =>null, - 'fieldField'=>null, - 'fieldMsg' =>null - ); - $caller = $this->getCaller(); - $methods = get_class_methods($caller); - if(!$methods) { - return $return; - } - foreach($methods as $method) { - if(!preg_match("#extendedRequiredFields#",$method)) { - continue; - } - // One of the DO's validation methods has failed - $extended = $caller->$method($this->getData()); - if($extended['fieldValid'] !== true) { - $return['fieldValid'] = $extended['fieldValid']; - $return['fieldName'] = $extended['fieldName']; - $return['fieldField'] = $extended['fieldField']; - $return['fieldMsg'] = $extended['fieldMsg']; - break; - } - } - return $return; - } + /** + * Allows for the addition of an arbitrary no. additional, dedicated and "extended" validation methods on classes that call AWRequiredFields. + * To add specific validation methods to a caller: + * + * 1). Write each checking method using this naming prototype: public function extendedRequiredFieldsXXX(). All methods so named will be called. + * 2). Call AWRequiredFields->setCaller($this) + * + * Each extended method thus called, should return an array of a specific format. (See: static $extendedMethodReturn on the caller) + * + * @return array $return + */ + public function getExtendedValidationRoutines() + { + // Setup a return array + $return = array( + 'fieldValid' => true, + 'fieldName' => null, + 'fieldField' => null, + 'fieldMsg' => null, + ); + $caller = $this->getCaller(); + $methods = get_class_methods($caller); + if (!$methods) { + return $return; + } + foreach ($methods as $method) { + if (!preg_match("#extendedRequiredFields#", $method)) { + continue; + } + // One of the DO's validation methods has failed + $extended = $caller->$method($this->getData()); + if ($extended['fieldValid'] !== true) { + $return['fieldValid'] = $extended['fieldValid']; + $return['fieldName'] = $extended['fieldName']; + $return['fieldField'] = $extended['fieldField']; + $return['fieldMsg'] = $extended['fieldMsg']; + break; + } + } + return $return; + } - protected function setData($data) { - $this->data = $data; - } + protected function setData($data) + { + $this->data = $data; + } - protected function getData() { - return $this->data; - } + protected function getData() + { + return $this->data; + } - public function setCaller($caller) { - self::$caller = $caller; - } + public function setCaller($caller) + { + self::$caller = $caller; + } - public function getCaller() { - return self::$caller; - } -} \ No newline at end of file + public function getCaller() + { + return self::$caller; + } +} diff --git a/code/forms/FrontendWorkflowForm.php b/code/forms/FrontendWorkflowForm.php index 9a4001f9..efd19480 100644 --- a/code/forms/FrontendWorkflowForm.php +++ b/code/forms/FrontendWorkflowForm.php @@ -1,136 +1,135 @@ requestVars(); + if (isset($funcName)) { + Form::setFormAction($funcName); + } + + // Populate the form + $this->loadDataFrom($vars, true); + + // Protection against CSRF attacks + $token = $this->getSecurityToken(); + if (!$token->checkRequest($request)) { + $this->httpError(400, _t('AdvancedWorkflowFrontendForm.SECURITYTOKENCHECK', "Security token doesn't match, possible CSRF attack.")); + } + + // Determine the action button clicked + $funcName = null; + foreach ($vars as $paramName => $paramVal) { + if (substr($paramName, 0, 7) == 'action_') { + // Added for frontend workflow form - get / set transitionID on controller, + // unset action and replace with doFrontEndAction action + if (substr($paramName, 0, 18) == 'action_transition_') { + $this->controller->transitionID = substr($paramName, strrpos($paramName, '_') +1); + unset($vars['action_transition_' . $this->controller->transitionID]); + $vars['action_doFrontEndAction'] = 'doFrontEndAction'; + $paramName = 'action_doFrontEndAction'; + $paramVal = 'doFrontEndAction'; + } + + // Break off querystring arguments included in the action + if (strpos($paramName, '?') !== false) { + list($paramName, $paramVars) = explode('?', $paramName, 2); + $newRequestParams = array(); + parse_str($paramVars, $newRequestParams); + $vars = array_merge((array)$vars, (array)$newRequestParams); + } + + // Cleanup action_, _x and _y from image fields + $funcName = preg_replace(array('/^action_/','/_x$|_y$/'), '', $paramName); + break; + } + } + + // If the action wasnt' set, choose the default on the form. + if (!isset($funcName) && $defaultAction = $this->defaultAction()) { + $funcName = $defaultAction->actionName(); + } + + if (isset($funcName)) { + $this->setButtonClicked($funcName); + } + + // Permission checks (first on controller, then falling back to form) + if (// Ensure that the action is actually a button or method on the form, + // and not just a method on the controller. + $this->controller->hasMethod($funcName) + && !$this->controller->checkAccessAction($funcName) + // If a button exists, allow it on the controller + && !$this->Actions()->fieldByName('action_' . $funcName) + ) { + return $this->httpError( + 403, + sprintf(_t('AdvancedWorkflowFrontendForm.ACTIONCONTROLLERCHECK', 'Action "%s" not allowed on controller (Class: %s)'), $funcName, get_class($this->controller)) + ); + } elseif ($this->hasMethod($funcName) + && !$this->checkAccessAction($funcName) + // No checks for button existence or $allowed_actions is performed - + // all form methods are callable (e.g. the legacy "callfieldmethod()") + ) { + return $this->httpError( + 403, + sprintf(_t('AdvancedWorkflowFrontendForm.ACTIONFORMCHECK', 'Action "%s" not allowed on form (Name: "%s")'), $funcName, $this->Name()) + ); + } + + if ($wfTransition = $this->controller->getCurrentTransition()) { + $wfTransType = $wfTransition->Type; + } else { + $wfTransType = null; //ie. when a custom Form Action is defined in WorkflowAction + } + + // Validate the form + if (!$this->validate() && $wfTransType == 'Active') { + if (Director::is_ajax()) { + $acceptType = $request->getHeader('Accept'); + if (strpos($acceptType, 'application/json') !== false) { + // Send validation errors back as JSON with a flag at the start + $response = new HTTPResponse(Convert::array2json($this->validator->getErrors())); + $response->addHeader('Content-Type', 'application/json'); + } else { + $this->setupFormErrors(); + // Send the newly rendered form tag as HTML + $response = new HTTPResponse($this->forTemplate()); + $response->addHeader('Content-Type', 'text/html'); + } + + return $response; + } + + if ($this->getRedirectToFormOnValidationError()) { + if ($pageURL = $request->getHeader('Referer')) { + if (Director::is_site_url($pageURL)) { + // Remove existing pragmas + $pageURL = preg_replace('/(#.*)/', '', $pageURL); + return Director::redirect($pageURL . '#' . $this->FormName()); + } + } + } + return $this->controller->redirectBack(); + } + + // First, try a handler method on the controller (has been checked for allowed_actions above already) + if ($this->controller->hasMethod($funcName)) { + return $this->controller->$funcName($vars, $this, $request); + // Otherwise, try a handler method on the form object. + } elseif ($this->hasMethod($funcName)) { + return $this->$funcName($vars, $this, $request); + } -class FrontendWorkflowForm extends Form{ - - function httpSubmission($request) { - $vars = $request->requestVars(); - if(isset($funcName)) { - Form::set_current_action($funcName); - } - - // Populate the form - $this->loadDataFrom($vars, true); - - // Protection against CSRF attacks - $token = $this->getSecurityToken(); - if(!$token->checkRequest($request)) { - $this->httpError(400, _t('AdvancedWorkflowFrontendForm.SECURITYTOKENCHECK', "Security token doesn't match, possible CSRF attack.")); - } - - // Determine the action button clicked - $funcName = null; - foreach($vars as $paramName => $paramVal) { - if(substr($paramName,0,7) == 'action_') { - - // Added for frontend workflow form - get / set transitionID on controller, - // unset action and replace with doFrontEndAction action - if(substr($paramName,0,18) == 'action_transition_') { - $this->controller->transitionID = substr($paramName,strrpos($paramName,'_') +1); - unset($vars['action_transition_' . $this->controller->transitionID]); - $vars['action_doFrontEndAction'] = 'doFrontEndAction'; - $paramName = 'action_doFrontEndAction'; - $paramVal = 'doFrontEndAction'; - } - - // Break off querystring arguments included in the action - if(strpos($paramName,'?') !== false) { - list($paramName, $paramVars) = explode('?', $paramName, 2); - $newRequestParams = array(); - parse_str($paramVars, $newRequestParams); - $vars = array_merge((array)$vars, (array)$newRequestParams); - } - - // Cleanup action_, _x and _y from image fields - $funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName); - break; - } - } - - // If the action wasnt' set, choose the default on the form. - if(!isset($funcName) && $defaultAction = $this->defaultAction()){ - $funcName = $defaultAction->actionName(); - } - - if(isset($funcName)) { - $this->setButtonClicked($funcName); - } - - // Permission checks (first on controller, then falling back to form) - if( - // Ensure that the action is actually a button or method on the form, - // and not just a method on the controller. - $this->controller->hasMethod($funcName) - && !$this->controller->checkAccessAction($funcName) - // If a button exists, allow it on the controller - && !$this->Actions()->fieldByName('action_' . $funcName) - ) { - return $this->httpError( - 403, - sprintf(_t('AdvancedWorkflowFrontendForm.ACTIONCONTROLLERCHECK', 'Action "%s" not allowed on controller (Class: %s)'), $funcName, get_class($this->controller)) - ); - } elseif( - $this->hasMethod($funcName) - && !$this->checkAccessAction($funcName) - // No checks for button existence or $allowed_actions is performed - - // all form methods are callable (e.g. the legacy "callfieldmethod()") - ) { - return $this->httpError( - 403, - sprintf(_t('AdvancedWorkflowFrontendForm.ACTIONFORMCHECK','Action "%s" not allowed on form (Name: "%s")'), $funcName, $this->Name()) - ); - } - - if ($wfTransition = $this->controller->getCurrentTransition()) { - $wfTransType = $wfTransition->Type; - } else { - $wfTransType = null; //ie. when a custom Form Action is defined in WorkflowAction - } - - // Validate the form - if(!$this->validate() && $wfTransType == 'Active') { - if(Director::is_ajax()) { - // Special case for legacy Validator.js implementation (assumes eval'ed javascript collected through FormResponse) - if($this->validator->getJavascriptValidationHandler() == 'prototype') { - return FormResponse::respond(); - } else { - $acceptType = $request->getHeader('Accept'); - if(strpos($acceptType, 'application/json') !== FALSE) { - // Send validation errors back as JSON with a flag at the start - $response = new SS_HTTPResponse(Convert::array2json($this->validator->getErrors())); - $response->addHeader('Content-Type', 'application/json'); - } else { - $this->setupFormErrors(); - // Send the newly rendered form tag as HTML - $response = new SS_HTTPResponse($this->forTemplate()); - $response->addHeader('Content-Type', 'text/html'); - } - - return $response; - } - } else { - if($this->getRedirectToFormOnValidationError()) { - if($pageURL = $request->getHeader('Referer')) { - if(Director::is_site_url($pageURL)) { - // Remove existing pragmas - $pageURL = preg_replace('/(#.*)/', '', $pageURL); - return Director::redirect($pageURL . '#' . $this->FormName()); - } - } - } - return $this->controller->redirectBack(); - } - } - - // First, try a handler method on the controller (has been checked for allowed_actions above already) - if($this->controller->hasMethod($funcName)) { - return $this->controller->$funcName($vars, $this, $request); - // Otherwise, try a handler method on the form object. - } elseif($this->hasMethod($funcName)) { - return $this->$funcName($vars, $this, $request); - } - - return $this->httpError(404); - } -} \ No newline at end of file + return $this->httpError(404); + } +} diff --git a/code/forms/gridfield/GridFieldExportAction.php b/code/forms/gridfield/GridFieldExportAction.php index 733176be..8050a644 100644 --- a/code/forms/gridfield/GridFieldExportAction.php +++ b/code/forms/gridfield/GridFieldExportAction.php @@ -1,9 +1,17 @@ 'col-buttons'); - } + /** + * Return any special attributes that will be used for FormField::create_tag() + * + * @param GridField $gridField + * @param DataObject $record + * @param string $columnName + * @return array + */ + public function getColumnAttributes($gridField, $record, $columnName) + { + return array('class' => 'col-buttons'); + } - /** - * Add the title - * - * @param GridField $gridField - * @param string $columnName - * @return array - */ - public function getColumnMetadata($gridField, $columnName) { - if($columnName == 'Actions') { - return array('title' => ''); - } - } + /** + * Add the title + * + * @param GridField $gridField + * @param string $columnName + * @return array + */ + public function getColumnMetadata($gridField, $columnName) + { + if ($columnName == 'Actions') { + return array('title' => ''); + } + } - /** - * Which columns are handled by this component - * - * @param type $gridField - * @return type - */ - public function getColumnsHandled($gridField) { - return array('Actions'); - } + /** + * Which columns are handled by this component + * + * @param type $gridField + * @return type + */ + public function getColumnsHandled($gridField) + { + return array('Actions'); + } - /** - * Which GridField actions are this component handling - * - * @param GridField $gridField - * @return array - */ - public function getActions($gridField) { - return array('exportrecord'); - } + /** + * Which GridField actions are this component handling + * + * @param GridField $gridField + * @return array + */ + public function getActions($gridField) + { + return array('exportrecord'); + } - /** - * - * @param GridField $gridField - * @param DataObject $record - * @param string $columnName - * @return string - the HTML for the column - */ - public function getColumnContent($gridField, $record, $columnName) { - // Disable the export icon if current user doesn't have access to view CMS Security settings - if(!Permission::check('CMS_ACCESS_SecurityAdmin')) { - return ''; - } + /** + * + * @param GridField $gridField + * @param DataObject $record + * @param string $columnName + * @return string - the HTML for the column + */ + public function getColumnContent($gridField, $record, $columnName) + { + // Disable the export icon if current user doesn't have access to view CMS Security settings + if (!Permission::check('CMS_ACCESS_SecurityAdmin')) { + return ''; + } - $field = GridField_FormAction::create($gridField, 'ExportRecord'.$record->ID, false, "exportrecord", - array('RecordID' => $record->ID)) - ->addExtraClass('gridfield-button-export') - ->setAttribute('title', _t('GridAction.Export', "Export")) - ->setAttribute('data-icon', 'export') - ->setDescription(_t('GridAction.EXPORT_DESCRIPTION','Export')); + $field = GridField_FormAction::create( + $gridField, + 'ExportRecord'.$record->ID, + false, + "exportrecord", + array('RecordID' => $record->ID) + ) + ->addExtraClass('gridfield-button-export') + ->setAttribute('title', _t('GridAction.Export', "Export")) + ->setAttribute('data-icon', 'export') + ->setDescription(_t('GridAction.EXPORT_DESCRIPTION', 'Export')); - $segment1 = Director::baseURL(); - $segment2 = Config::inst()->get('AdvancedWorkflowAdmin', 'url_segment'); - $segment3 = $record->getClassName(); - $fields = new ArrayData(array( - 'Link' => Controller::join_links($segment1, 'admin', $segment2 , $segment3, 'export', $record->ID) - )); + $segment1 = Director::baseURL(); + $segment2 = Config::inst()->get(AdvancedWorkflowAdmin::class, 'url_segment'); + $segment3 = $record->getClassName(); + $fields = new ArrayData(array( + 'Link' => Controller::join_links($segment1, 'admin', $segment2, $segment3, 'export', $record->ID) + )); - return $field->Field()->renderWith('Includes/GridField_ExportAction', $fields); - } + return $field->Field()->renderWith('Includes/GridField_ExportAction', $fields); + } - /** - * Handle the actions and apply any changes to the GridField - * - * @param GridField $gridField - * @param string $actionName - * @param mixed $arguments - * @param array $data - form data - * @return void - */ - public function handleAction(GridField $gridField, $actionName, $arguments, $data) { - } + /** + * Handle the actions and apply any changes to the GridField + * + * @param GridField $gridField + * @param string $actionName + * @param mixed $arguments + * @param array $data - form data + * @return void + */ + public function handleAction(GridField $gridField, $actionName, $arguments, $data) + { + } } diff --git a/code/forms/gridfield/GridFieldWorkflowRestrictedEditButton.php b/code/forms/gridfield/GridFieldWorkflowRestrictedEditButton.php index 9e387d55..42fa35eb 100644 --- a/code/forms/gridfield/GridFieldWorkflowRestrictedEditButton.php +++ b/code/forms/gridfield/GridFieldWorkflowRestrictedEditButton.php @@ -1,86 +1,97 @@ 'col-buttons'); - if($record instanceof WorkflowInstance) { - $isAdmin = Permission::check('ADMIN'); - $isAssigned = $record->getAssignedMembers()->find('ID', Member::currentUserID()); - if(!$isAdmin && !$isAssigned) { - $atts['class'] = $defaultAtts['class'].' disabled'; - return $atts; - } - return $defaultAtts; - } - return $defaultAtts; - } + /** + * Append a 'disabled' CSS class to GridField rows whose WorkflowInstance records are not viewable/editable + * by the current user. + * + * This is used to visually "grey out" records and it's leveraged in some overriding JavaScript, to maintain an ability + * to click the target object's hyperlink. + * + * @param GridField $gridField + * @param DataObject $record + * @param string $columnName + * @return array + */ + public function getColumnAttributes($gridField, $record, $columnName) + { + $defaultAtts = array('class' => 'col-buttons'); + if ($record instanceof WorkflowInstance) { + $isAdmin = Permission::check('ADMIN'); + $isAssigned = $record->getAssignedMembers()->find('ID', Security::getCurrentUser()->ID); + if (!$isAdmin && !$isAssigned) { + $atts['class'] = $defaultAtts['class'].' disabled'; + return $atts; + } + return $defaultAtts; + } + return $defaultAtts; + } - /** - * Add the title - * - * @param GridField $gridField - * @param string $columnName - * @return array - */ - public function getColumnMetadata($gridField, $columnName) { - if($columnName == 'Actions') { - return array('title' => ''); - } - } + /** + * Add the title + * + * @param GridField $gridField + * @param string $columnName + * @return array + */ + public function getColumnMetadata($gridField, $columnName) + { + if ($columnName == 'Actions') { + return array('title' => ''); + } + } - /** - * Which columns are handled by this component - * - * @param type $gridField - * @return type - */ - public function getColumnsHandled($gridField) { - return array('Actions'); - } + /** + * Which columns are handled by this component + * + * @param type $gridField + * @return type + */ + public function getColumnsHandled($gridField) + { + return array('Actions'); + } - /** - * @param GridField $gridField - * @param DataObject $record - * @param string $columnName - * - * @return string - the HTML for the column - */ - public function getColumnContent($gridField, $record, $columnName) { - $data = new ArrayData(array( - 'Link' => Controller::join_links($gridField->Link('item'), $record->ID, 'edit') - )); - return $data->renderWith('Includes/GridFieldEditButton'); - } + /** + * @param GridField $gridField + * @param DataObject $record + * @param string $columnName + * + * @return string - the HTML for the column + */ + public function getColumnContent($gridField, $record, $columnName) + { + $data = new ArrayData(array( + 'Link' => Controller::join_links($gridField->Link('item'), $record->ID, 'edit') + )); + return $data->renderWith('Includes/GridFieldEditButton'); + } } diff --git a/code/jobs/WorkflowPublishTargetJob.php b/code/jobs/WorkflowPublishTargetJob.php index c7924403..ac7ae3e4 100644 --- a/code/jobs/WorkflowPublishTargetJob.php +++ b/code/jobs/WorkflowPublishTargetJob.php @@ -1,7 +1,11 @@ setObject($obj); - $this->publishType = $type ? strtolower($type) : 'publish'; - $this->totalSteps = 1; - } - } - - public function getTitle() { - return _t( - 'AdvancedWorkflowPublishJob.SCHEDULEJOBTITLE', - "Scheduled {type} of {object}", - "", - array( - 'type' => $this->publishType, - 'object' => $this->getObject()->Title - ) - ); - } +class WorkflowPublishTargetJob extends AbstractQueuedJob +{ + public function __construct($obj = null, $type = null) + { + if ($obj) { + $this->setObject($obj); + $this->publishType = $type ? strtolower($type) : 'publish'; + $this->totalSteps = 1; + } + } - public function process() { - if ($target = $this->getObject()) { - if ($this->publishType == 'publish') { - $target->setIsPublishJobRunning(true); - $target->PublishOnDate = ''; - $target->writeWithoutVersion(); - $target->doPublish(); - } else if ($this->publishType == 'unpublish') { - $target->setIsPublishJobRunning(true); - $target->UnPublishOnDate = ''; - $target->writeWithoutVersion(); - $target->doUnpublish(); - } - } - $this->currentStep = 1; - $this->isComplete = true; - } + public function getTitle() + { + return _t( + 'AdvancedWorkflowPublishJob.SCHEDULEJOBTITLE', + "Scheduled {type} of {object}", + "", + array( + 'type' => $this->publishType, + 'object' => $this->getObject()->Title + ) + ); + } + public function process() + { + if ($target = $this->getObject()) { + if ($this->publishType == 'publish') { + $target->setIsPublishJobRunning(true); + $target->PublishOnDate = ''; + $target->writeWithoutVersion(); + $target->publishRecursive(); + } elseif ($this->publishType == 'unpublish') { + $target->setIsPublishJobRunning(true); + $target->UnPublishOnDate = ''; + $target->writeWithoutVersion(); + $target->doUnpublish(); + } + } + $this->currentStep = 1; + $this->isComplete = true; + } } diff --git a/code/jobs/WorkflowReminderJob.php b/code/jobs/WorkflowReminderJob.php index 836fd9a8..fd64b479 100644 --- a/code/jobs/WorkflowReminderJob.php +++ b/code/jobs/WorkflowReminderJob.php @@ -1,101 +1,118 @@ * @license BSD License http://www.silverstripe.org/bsd-license */ -class WorkflowReminderJob extends AbstractQueuedJob { - const DEFAULT_REPEAT = 600; - - /** - * - * @var QueuedJobService - */ - public $queuedJobService; - - public function __construct($repeatInterval = 0) { - if (!$this->repeatInterval) { - $this->repeatInterval = $repeatInterval ? $repeatInterval : self::DEFAULT_REPEAT; - $this->totalSteps = 2; - $this->currentStep = 1; - } - } - - public function getTitle() { - return _t('AdvancedWorkflow.WORKFLOW_REMINDER_JOB', 'Workflow Reminder Job'); - } - - /** - * We only want one instance of this job ever - * - * @return string - */ - public function getSignature() { - return md5($this->getTitle()); - } - - public function process() { - $sent = 0; - $filter = array( - 'WorkflowStatus' => array('Active', 'Paused'), - 'Definition.RemindDays:GreaterThan' => 0 - ); - - $active = WorkflowInstance::get()->filter($filter); - - foreach ($active as $instance) { - $edited = strtotime($instance->LastEdited); - $days = $instance->Definition()->RemindDays; - - if ($edited + ($days * 3600 * 24) > time()) { - continue; - } - - $email = new Email(); - $bcc = ''; - $members = $instance->getAssignedMembers(); - $target = $instance->getTarget(); - - if (!$members || !count($members)) { - continue; - } - - $email->setSubject("Workflow Reminder: $instance->Title"); - $email->setBcc(implode(', ', $members->column('Email'))); - $email->setTemplate('WorkflowReminderEmail'); - $email->populateTemplate(array( - 'Instance' => $instance, - 'Link' => $target instanceof SiteTree ? "admin/show/$target->ID" : '' - )); - - $email->send(); - $sent++; - - // add a comment to the workflow if possible - $action = $instance->CurrentAction(); - - $currentComment = $action->Comment; - $action->Comment = sprintf(_t('AdvancedWorkflow.JOB_REMINDER_COMMENT', '%s: Reminder email sent\n\n'), date('Y-m-d H:i:s')) . $currentComment; - try { - $action->write(); - } catch (Exception $ex) { - SS_Log::log($ex, SS_Log::WARN); - } - - $instance->LastEdited = time(); - try { - $instance->write(); - } catch (Exception $ex) { - SS_Log::log($ex, SS_Log::WARN); - } - } - - $this->currentStep = 2; - $this->isComplete = true; - - $nextDate = date('Y-m-d H:i:s', time() + $this->repeatInterval); - $this->queuedJobService->queueJob(new WorkflowReminderJob($this->repeatInterval), $nextDate); - } +class WorkflowReminderJob extends AbstractQueuedJob +{ + const DEFAULT_REPEAT = 600; + + /** + * + * @var QueuedJobService + */ + public $queuedJobService; + + public function __construct($repeatInterval = 0) + { + if (!$this->repeatInterval) { + $this->repeatInterval = $repeatInterval ? $repeatInterval : self::DEFAULT_REPEAT; + $this->totalSteps = 2; + $this->currentStep = 1; + } + } + + public function getTitle() + { + return _t('AdvancedWorkflow.WORKFLOW_REMINDER_JOB', 'Workflow Reminder Job'); + } + + /** + * We only want one instance of this job ever + * + * @return string + */ + public function getSignature() + { + return md5($this->getTitle()); + } + + public function process() + { + $sent = 0; + $filter = array( + 'WorkflowStatus' => array('Active', 'Paused'), + 'Definition.RemindDays:GreaterThan' => 0 + ); + + $active = WorkflowInstance::get()->filter($filter); + + foreach ($active as $instance) { + $edited = strtotime($instance->LastEdited); + $days = $instance->Definition()->RemindDays; + + if ($edited + ($days * 3600 * 24) > time()) { + continue; + } + + $email = new Email(); + $bcc = ''; + $members = $instance->getAssignedMembers(); + $target = $instance->getTarget(); + + if (!$members || !count($members)) { + continue; + } + + $email->setSubject("Workflow Reminder: $instance->Title"); + $email->setBcc(implode(', ', $members->column(Email::class))); + $email->setHTMLTemplate('WorkflowReminderEmail'); + $email->setData(array( + 'Instance' => $instance, + 'Link' => $target instanceof SiteTree ? "admin/show/$target->ID" : '' + )); + + $email->send(); + $sent++; + + // add a comment to the workflow if possible + $action = $instance->CurrentAction(); + + $currentComment = $action->Comment; + $action->Comment = sprintf(_t('AdvancedWorkflow.JOB_REMINDER_COMMENT', '%s: Reminder email sent\n\n'), date('Y-m-d H:i:s')) . $currentComment; + try { + $action->write(); + } catch (Exception $ex) { + Injector::inst()->get(LoggerInterface::class)->warning($ex->getMessage()); + } + + $instance->LastEdited = time(); + try { + $instance->write(); + } catch (Exception $ex) { + Injector::inst()->get(LoggerInterface::class)->warning($ex->getMessage()); + } + } + + $this->currentStep = 2; + $this->isComplete = true; + + $nextDate = date('Y-m-d H:i:s', time() + $this->repeatInterval); + $this->queuedJobService->queueJob(new WorkflowReminderJob($this->repeatInterval), $nextDate); + } } -} \ No newline at end of file diff --git a/code/services/ExistingWorkflowException.php b/code/services/ExistingWorkflowException.php new file mode 100644 index 00000000..da3ae001 --- /dev/null +++ b/code/services/ExistingWorkflowException.php @@ -0,0 +1,9 @@ +templates = $templates; - } - - /** - * Return the list of available templates - * @return type - */ - public function getTemplates() { - return $this->templates; - } - - /** - * Get a template by name - * - * @param string $name - * @return WorkflowTemplate - */ - public function getNamedTemplate($name) { - if($importedTemplate = singleton('WorkflowDefinitionImporter')->getImportedWorkflows($name)) { - return $importedTemplate; - } - - if (!is_array($this->templates)) { - return; - } - foreach ($this->templates as $template) { - if ($template->getName() == $name) { - return $template; - } - } - } - - /** - * Gets the workflow definition for a given dataobject, if there is one - * - * Will recursively query parent elements until it finds one, if available - * - * @param DataObject $dataObject - */ - public function getDefinitionFor(DataObject $dataObject) { - if ($dataObject->hasExtension('WorkflowApplicable') || $dataObject->hasExtension('FileWorkflowApplicable')) { - if ($dataObject->WorkflowDefinitionID) { - return DataObject::get_by_id('WorkflowDefinition', $dataObject->WorkflowDefinitionID); - } - if ($dataObject->hasMethod('useInheritedWorkflow') && !$dataObject->useInheritedWorkflow()) { - return null; - } - if ($dataObject->ParentID) { - return $this->getDefinitionFor($dataObject->Parent()); - } - if ($dataObject->hasMethod('workflowParent')) { - $obj = $dataObject->workflowParent(); - if ($obj) { - return $this->getDefinitionFor($obj); - } - } - } - return null; - } - - /** - * Retrieves a workflow definition by ID for a data object. - * - * @param data object - * @param integer - * @return workflow definition - */ - - public function getDefinitionByID($object, $workflowID) { - - // Make sure the correct extensions have been applied to the data object. - - $workflow = null; - if($object->hasExtension('WorkflowApplicable') || $object->hasExtension('FileWorkflowApplicable')) { - - // Validate the workflow ID against the data object. - - if(($object->WorkflowDefinitionID == $workflowID) || ($workflow = $object->AdditionalWorkflowDefinitions()->byID($workflowID))) { - if(is_null($workflow)) { - $workflow = DataObject::get_by_id('WorkflowDefinition', $workflowID); - } - } - } - return $workflow ? $workflow : null; - } - - /** - * Retrieves and collates the workflow definitions for a data object, where the first element will be the main workflow definition. - * - * @param data object - * @return array - */ - - public function getDefinitionsFor($object) { - - // Retrieve the main workflow definition. - - $default = $this->getDefinitionFor($object); - if($default) { - - // Merge the additional workflow definitions. - - return array_merge(array( - $default - ), $object->AdditionalWorkflowDefinitions()->toArray()); - } - return null; - } - - /** - * Gets the workflow for the given item - * - * The item can be - * - * a data object in which case the ActiveWorkflow will be returned, - * an action, in which case the Workflow will be returned - * an integer, in which case the workflow with that ID will be returned - * - * @param mixed $item - * - * @return WorkflowInstance - */ - public function getWorkflowFor($item, $includeComplete = false) { - $id = $item; - - if ($item instanceof WorkflowAction) { - $id = $item->WorkflowID; - return DataObject::get_by_id('WorkflowInstance', $id); - } else if (is_object($item) && ($item->hasExtension('WorkflowApplicable') || $item->hasExtension('FileWorkflowApplicable'))) { - $filter = sprintf('"TargetClass" = \'%s\' AND "TargetID" = %d', ClassInfo::baseDataClass($item), $item->ID); - $complete = $includeComplete ? 'OR "WorkflowStatus" = \'Complete\' ' : ''; - return DataObject::get_one('WorkflowInstance', $filter . ' AND ("WorkflowStatus" = \'Active\' OR "WorkflowStatus"=\'Paused\' ' . $complete . ')'); - } - } - - /** - * Get all the workflow action instances for an item - * - * @return DataObjectSet - */ - public function getWorkflowHistoryFor($item, $limit = null){ - if($active = $this->getWorkflowFor($item, true)){ - $limit = $limit ? "0,$limit" : ''; - return $active->Actions('', 'ID DESC ', null, $limit); - } - } - - /** - * Get all the available workflow definitions - * - * @return DataList - */ - public function getDefinitions() { - return DataList::create('WorkflowDefinition'); - } - - /** - * Given a transition ID, figure out what should happen to - * the given $subject. - * - * In the normal case, this will load the current workflow instance for the object - * and then transition as expected. However, in some cases (eg to start the workflow) - * it is necessary to instead create a new instance. - * - * @param DataObject $target - * @param int $transitionId - */ - public function executeTransition(DataObject $target, $transitionId) { - $workflow = $this->getWorkflowFor($target); - $transition = DataObject::get_by_id('WorkflowTransition', $transitionId); - - if(!$transition) { - throw new Exception(_t('WorkflowService.INVALID_TRANSITION_ID', "Invalid transition ID $transitionId")); - } - - if(!$workflow) { - throw new Exception(_t('WorkflowService.INVALID_WORKFLOW_TARGET', "A transition was executed on a target that does not have a workflow.")); - } - - if($transition->Action()->WorkflowDefID != $workflow->DefinitionID) { - throw new Exception(_t('WorkflowService.INVALID_TRANSITION_WORKFLOW', "Transition #$transition->ID is not attached to workflow #$workflow->ID.")); - } - - $workflow->performTransition($transition); - } - - /** - * Starts the workflow for the given data object, assuming it or a parent has - * a definition specified. - * - * @param DataObject $object - */ - public function startWorkflow(DataObject $object, $workflowID = null) { - $existing = $this->getWorkflowFor($object); - if ($existing) { - throw new ExistingWorkflowException(_t('WorkflowService.EXISTING_WORKFLOW_ERROR', "That object already has a workflow running")); - } - - $definition = null; - if($workflowID) { - - // Retrieve the workflow definition that has been triggered. - - $definition = $this->getDefinitionByID($object, $workflowID); - } - if(is_null($definition)) { - - // Fall back to the main workflow definition. - - $definition = $this->getDefinitionFor($object); - } - - if ($definition) { - $instance = new WorkflowInstance(); - $instance->beginWorkflow($definition, $object); - $instance->execute(); - } - } - - /** - * Get all the workflows that this user is responsible for - * - * @param Member $user - * The user to get workflows for - * - * @return ArrayList - * The list of workflow instances this user owns - */ - public function usersWorkflows(Member $user) { - - $groupIds = $user->Groups()->column('ID'); - - $groupInstances = null; - - $filter = array(''); - - if (is_array($groupIds)) { - $groupInstances = DataList::create('WorkflowInstance') - ->filter(array('Group.ID:ExactMatchMulti' => $groupIds)) - ->where('"WorkflowStatus" != \'Complete\''); - } - - $userInstances = DataList::create('WorkflowInstance') - ->filter(array('Users.ID:ExactMatch' => $user->ID)) - ->where('"WorkflowStatus" != \'Complete\''); - - if ($userInstances) { - $userInstances = $userInstances->toArray(); - } else { - $userInstances = array(); - } - - if ($groupInstances) { - $groupInstances = $groupInstances->toArray(); - } else { - $groupInstances = array(); - } - - $all = array_merge($groupInstances, $userInstances); - - return ArrayList::create($all); - } - - /** - * Get items that the passed-in user has awaiting for them to action - * - * @param Member $member - * @return DataList $userInstances - */ - public function userPendingItems(Member $user) { - // Don't restrict anything for ADMIN users - $userInstances = DataList::create('WorkflowInstance') - ->where('"WorkflowStatus" != \'Complete\'') - ->sort('LastEdited DESC'); - - if(Permission::checkMember($user, 'ADMIN')) { - return $userInstances; - } - $instances = new ArrayList(); - foreach($userInstances as $inst) { - $instToArray = $inst->getAssignedMembers(); - if(!count($instToArray)>0 || !in_array($user->ID,$instToArray->column())) { - continue; - } - $instances->push($inst); - } - - return $instances; - } - - /** - * Get items that the passed-in user has submitted for workflow review - * - * @param Member $member - * @return DataList $userInstances - */ - public function userSubmittedItems(Member $user) { - $userInstances = DataList::create('WorkflowInstance') - ->where('"WorkflowStatus" != \'Complete\'') - ->sort('LastEdited DESC'); - - // Restrict the user if they're not an ADMIN. - if(!Permission::checkMember($user, 'ADMIN')) { - $userInstances = $userInstances->filter('InitiatorID:ExactMatch', $user->ID); - } - - return $userInstances; - } - - /** - * Generate a workflow definition based on a template - * - * @param WorkflowDefinition $definition - * @param string $templateName - */ - public function defineFromTemplate(WorkflowDefinition $definition, $templateName) { - $template = null; - /* @var $template WorkflowTemplate */ - - if (!is_array($this->templates)) { - return; - } - - $template = $this->getNamedTemplate($templateName); - - if (!$template) { - return; - } - - $template->createRelations($definition); - - // Set the version and do the write at the end so that we don't trigger an infinite loop!! +class WorkflowService implements PermissionProvider +{ + /** + * An array of templates that we can create from + * + * @var array + */ + protected $templates; + + /** + * Set the list of templates that can be created + * + * @param array $templates + */ + public function setTemplates($templates) + { + $this->templates = $templates; + } + + /** + * Return the list of available templates + * @return array + */ + public function getTemplates() + { + return $this->templates; + } + + /** + * Get a template by name + * + * @param string $name + * @return WorkflowTemplate|null + */ + public function getNamedTemplate($name) + { + if ($importedTemplate = singleton(WorkflowDefinitionImporter::class)->getImportedWorkflows($name)) { + return $importedTemplate; + } + + if (!is_array($this->templates)) { + return; + } + foreach ($this->templates as $template) { + if ($template->getName() == $name) { + return $template; + } + } + } + + /** + * Gets the workflow definition for a given dataobject, if there is one + * + * Will recursively query parent elements until it finds one, if available + * + * @param DataObject $dataObject + */ + public function getDefinitionFor(DataObject $dataObject) + { + if ($dataObject->hasExtension(WorkflowApplicable::class) || $dataObject->hasExtension(FileWorkflowApplicable::class)) { + if ($dataObject->WorkflowDefinitionID) { + return DataObject::get_by_id(WorkflowDefinition::class, $dataObject->WorkflowDefinitionID); + } + if ($dataObject->hasMethod('useInheritedWorkflow') && !$dataObject->useInheritedWorkflow()) { + return null; + } + if ($dataObject->ParentID) { + return $this->getDefinitionFor($dataObject->Parent()); + } + if ($dataObject->hasMethod('workflowParent')) { + $obj = $dataObject->workflowParent(); + if ($obj) { + return $this->getDefinitionFor($obj); + } + } + } + return null; + } + + /** + * Retrieves a workflow definition by ID for a data object. + * + * @param DataObject $object + * @param integer $workflowID + * @return WorkflowDefinition|null + */ + public function getDefinitionByID($object, $workflowID) + { + // Make sure the correct extensions have been applied to the data object. + + $workflow = null; + if ($object->hasExtension(WorkflowApplicable::class) || $object->hasExtension(FileWorkflowApplicable::class)) { + // Validate the workflow ID against the data object. + + if (($object->WorkflowDefinitionID == $workflowID) || ($workflow = $object->AdditionalWorkflowDefinitions()->byID($workflowID))) { + if (is_null($workflow)) { + $workflow = DataObject::get_by_id(WorkflowDefinition::class, $workflowID); + } + } + } + return $workflow ? $workflow : null; + } + + /** + * Retrieves and collates the workflow definitions for a data object, where the first element will be the main workflow definition. + * + * @param DataObject object + * @return array + */ + + public function getDefinitionsFor($object) + { + + // Retrieve the main workflow definition. + + $default = $this->getDefinitionFor($object); + if ($default) { + // Merge the additional workflow definitions. + + return array_merge(array( + $default + ), $object->AdditionalWorkflowDefinitions()->toArray()); + } + return null; + } + + /** + * Gets the workflow for the given item + * + * The item can be + * + * a data object in which case the ActiveWorkflow will be returned, + * an action, in which case the Workflow will be returned + * an integer, in which case the workflow with that ID will be returned + * + * @param mixed $item + * @param bool $includeComplete + * @return WorkflowInstance|null + */ + public function getWorkflowFor($item, $includeComplete = false) + { + $id = $item; + + if ($item instanceof WorkflowAction) { + $id = $item->WorkflowID; + return DataObject::get_by_id(WorkflowInstance::class, $id); + } elseif (is_object($item) && ($item->hasExtension(WorkflowApplicable::class) || $item->hasExtension(FileWorkflowApplicable::class))) { + $filter = sprintf('"TargetClass" = \'%s\' AND "TargetID" = %d', ClassInfo::baseDataClass($item), $item->ID); + $complete = $includeComplete ? 'OR "WorkflowStatus" = \'Complete\' ' : ''; + return DataObject::get_one(WorkflowInstance::class, $filter . ' AND ("WorkflowStatus" = \'Active\' OR "WorkflowStatus"=\'Paused\' ' . $complete . ')'); + } + } + + /** + * Get all the workflow action instances for an item + * + * @return DataList|null + */ + public function getWorkflowHistoryFor($item, $limit = null) + { + if ($active = $this->getWorkflowFor($item, true)) { + $limit = $limit ? "0,$limit" : ''; + return $active->Actions('', 'ID DESC ', null, $limit); + } + } + + /** + * Get all the available workflow definitions + * + * @return DataList + */ + public function getDefinitions() + { + return DataList::create(WorkflowDefinition::class); + } + + /** + * Given a transition ID, figure out what should happen to + * the given $subject. + * + * In the normal case, this will load the current workflow instance for the object + * and then transition as expected. However, in some cases (eg to start the workflow) + * it is necessary to instead create a new instance. + * + * @param DataObject $target + * @param int $transitionId + * @throws Exception + */ + public function executeTransition(DataObject $target, $transitionId) + { + $workflow = $this->getWorkflowFor($target); + $transition = DataObject::get_by_id(WorkflowTransition::class, $transitionId); + + if (!$transition) { + throw new Exception(_t('WorkflowService.INVALID_TRANSITION_ID', "Invalid transition ID $transitionId")); + } + + if (!$workflow) { + throw new Exception(_t('WorkflowService.INVALID_WORKFLOW_TARGET', "A transition was executed on a target that does not have a workflow.")); + } + + if ($transition->Action()->WorkflowDefID != $workflow->DefinitionID) { + throw new Exception(_t('WorkflowService.INVALID_TRANSITION_WORKFLOW', "Transition #$transition->ID is not attached to workflow #$workflow->ID.")); + } + + $workflow->performTransition($transition); + } + + /** + * Starts the workflow for the given data object, assuming it or a parent has + * a definition specified. + * + * @param DataObject $object + * @param int $workflowID + */ + public function startWorkflow(DataObject $object, $workflowID = null) + { + $existing = $this->getWorkflowFor($object); + if ($existing) { + throw new ExistingWorkflowException(_t('WorkflowService.EXISTING_WORKFLOW_ERROR', "That object already has a workflow running")); + } + + $definition = null; + if ($workflowID) { + // Retrieve the workflow definition that has been triggered. + + $definition = $this->getDefinitionByID($object, $workflowID); + } + if (is_null($definition)) { + // Fall back to the main workflow definition. + + $definition = $this->getDefinitionFor($object); + } + + if ($definition) { + $instance = new WorkflowInstance(); + $instance->beginWorkflow($definition, $object); + $instance->execute(); + } + } + + /** + * Get all the workflows that this user is responsible for + * + * @param Member $user The user to get workflows for + * @return ArrayList The list of workflow instances this user owns + */ + public function usersWorkflows(Member $user) + { + + $groupIds = $user->Groups()->column('ID'); + + $groupInstances = null; + + $filter = array(''); + + if (is_array($groupIds)) { + $groupInstances = DataList::create(WorkflowInstance::class) + ->filter(array('Group.ID:ExactMatchMulti' => $groupIds)) + ->where('"WorkflowStatus" != \'Complete\''); + } + + $userInstances = DataList::create(WorkflowInstance::class) + ->filter(array('Users.ID:ExactMatch' => $user->ID)) + ->where('"WorkflowStatus" != \'Complete\''); + + if ($userInstances) { + $userInstances = $userInstances->toArray(); + } else { + $userInstances = array(); + } + + if ($groupInstances) { + $groupInstances = $groupInstances->toArray(); + } else { + $groupInstances = array(); + } + + $all = array_merge($groupInstances, $userInstances); + + return ArrayList::create($all); + } + + /** + * Get items that the passed-in user has awaiting for them to action + * + * @param Member $member + * @return DataList + */ + public function userPendingItems(Member $user) + { + // Don't restrict anything for ADMIN users + $userInstances = DataList::create(WorkflowInstance::class) + ->where('"WorkflowStatus" != \'Complete\'') + ->sort('LastEdited DESC'); + + if (Permission::checkMember($user, 'ADMIN')) { + return $userInstances; + } + $instances = new ArrayList(); + foreach ($userInstances as $inst) { + $instToArray = $inst->getAssignedMembers(); + if (!count($instToArray)>0 || !in_array($user->ID, $instToArray->column())) { + continue; + } + $instances->push($inst); + } + + return $instances; + } + + /** + * Get items that the passed-in user has submitted for workflow review + * + * @param Member $member + * @return DataList + */ + public function userSubmittedItems(Member $user) + { + $userInstances = DataList::create(WorkflowInstance::class) + ->where('"WorkflowStatus" != \'Complete\'') + ->sort('LastEdited DESC'); + + // Restrict the user if they're not an ADMIN. + if (!Permission::checkMember($user, 'ADMIN')) { + $userInstances = $userInstances->filter('InitiatorID:ExactMatch', $user->ID); + } + + return $userInstances; + } + + /** + * Generate a workflow definition based on a template + * + * @param WorkflowDefinition $definition + * @param string $templateName + * @return WorkflowDefinition|null + */ + public function defineFromTemplate(WorkflowDefinition $definition, $templateName) + { + $template = null; + /* @var $template WorkflowTemplate */ + + if (!is_array($this->templates)) { + return; + } + + $template = $this->getNamedTemplate($templateName); + + if (!$template) { + return; + } + + $template->createRelations($definition); + + // Set the version and do the write at the end so that we don't trigger an infinite loop!! if (!$definition->Description) { $definition->Description = $template->getDescription(); } - $definition->TemplateVersion = $template->getVersion(); - $definition->RemindDays = $template->getRemindDays(); - $definition->Sort = $template->getSort(); - $definition->write(); - return $definition; - } - - /** - * Reorders actions within a definition - * - * @param WorkflowDefinition|WorkflowAction $objects - * The objects to be reordered - * @param array $newOrder - * An array of IDs of the actions in the order they should be. - */ - public function reorder($objects, $newOrder) { - $sortVals = array_values($objects->map('ID', 'Sort')->toArray()); - sort($sortVals); - - // save the new ID values - but only use existing sort values to prevent - // conflicts with items not in the table - foreach($newOrder as $key => $id) { - if (!$id) { - continue; - } - $object = $objects->find('ID', $id); - $object->Sort = $sortVals[$key]; - $object->write(); - } - } - - /** - * - * @return array - */ - public function providePermissions() { - return array( - 'CREATE_WORKFLOW' => array( - 'name' => _t('AdvancedWorkflow.CREATE_WORKFLOW', 'Create workflow'), - 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), - 'help' => _t('AdvancedWorkflow.CREATE_WORKFLOW_HELP', 'Users can create workflow definitions'), - 'sort' => 0 - ), - 'DELETE_WORKFLOW' => array( - 'name' => _t('AdvancedWorkflow.DELETE_WORKFLOW', 'Delete workflow'), - 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), - 'help' => _t('AdvancedWorkflow.DELETE_WORKFLOW_HELP', 'Users can delete workflow definitions and active workflows'), - 'sort' => 1 - ), - 'APPLY_WORKFLOW' => array( - 'name' => _t('AdvancedWorkflow.APPLY_WORKFLOW', 'Apply workflow'), - 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), - 'help' => _t('AdvancedWorkflow.APPLY_WORKFLOW_HELP', 'Users can apply workflows to items'), - 'sort' => 2 - ), - 'VIEW_ACTIVE_WORKFLOWS' => array( - 'name' => _t('AdvancedWorkflow.VIEWACTIVE', 'View active workflows'), - 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), - 'help' => _t('AdvancedWorkflow.VIEWACTIVEHELP', 'Users can view active workflows via the workflows admin panel'), - 'sort' => 3 - ), - 'REASSIGN_ACTIVE_WORKFLOWS' => array( - 'name' => _t('AdvancedWorkflow.REASSIGNACTIVE', 'Reassign active workflows'), - 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), - 'help' => _t('AdvancedWorkflow.REASSIGNACTIVEHELP', 'Users can reassign active workflows to different users and groups'), - 'sort' => 4 - ), + $definition->TemplateVersion = $template->getVersion(); + $definition->RemindDays = $template->getRemindDays(); + $definition->Sort = $template->getSort(); + $definition->write(); + return $definition; + } + + /** + * Reorders actions within a definition + * + * @param WorkflowDefinition|WorkflowAction $objects The objects to be reordered + * @param array $newOrder An array of IDs of the actions in the order they should be. + */ + public function reorder($objects, $newOrder) + { + $sortVals = array_values($objects->map('ID', 'Sort')->toArray()); + sort($sortVals); + + // save the new ID values - but only use existing sort values to prevent + // conflicts with items not in the table + foreach ($newOrder as $key => $id) { + if (!$id) { + continue; + } + $object = $objects->find('ID', $id); + $object->Sort = $sortVals[$key]; + $object->write(); + } + } + + /** + * + * @return array + */ + public function providePermissions() + { + return array( + 'CREATE_WORKFLOW' => array( + 'name' => _t('AdvancedWorkflow.CREATE_WORKFLOW', 'Create workflow'), + 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), + 'help' => _t('AdvancedWorkflow.CREATE_WORKFLOW_HELP', 'Users can create workflow definitions'), + 'sort' => 0 + ), + 'DELETE_WORKFLOW' => array( + 'name' => _t('AdvancedWorkflow.DELETE_WORKFLOW', 'Delete workflow'), + 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), + 'help' => _t('AdvancedWorkflow.DELETE_WORKFLOW_HELP', 'Users can delete workflow definitions and active workflows'), + 'sort' => 1 + ), + 'APPLY_WORKFLOW' => array( + 'name' => _t('AdvancedWorkflow.APPLY_WORKFLOW', 'Apply workflow'), + 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), + 'help' => _t('AdvancedWorkflow.APPLY_WORKFLOW_HELP', 'Users can apply workflows to items'), + 'sort' => 2 + ), + 'VIEW_ACTIVE_WORKFLOWS' => array( + 'name' => _t('AdvancedWorkflow.VIEWACTIVE', 'View active workflows'), + 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), + 'help' => _t('AdvancedWorkflow.VIEWACTIVEHELP', 'Users can view active workflows via the workflows admin panel'), + 'sort' => 3 + ), + 'REASSIGN_ACTIVE_WORKFLOWS' => array( + 'name' => _t('AdvancedWorkflow.REASSIGNACTIVE', 'Reassign active workflows'), + 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), + 'help' => _t('AdvancedWorkflow.REASSIGNACTIVEHELP', 'Users can reassign active workflows to different users and groups'), + 'sort' => 4 + ), 'EDIT_EMBARGOED_WORKFLOW' => array( 'name' => _t('AdvancedWorkflow.EDITEMBARGO', 'Editable embargoed item in workflow'), 'category' => _t('AdvancedWorkflow.ADVANCED_WORKFLOW', 'Advanced Workflow'), 'help' => _t('AdvancedWorkflow.EDITEMBARGOHELP', 'Allow users to edit items that have been embargoed by a workflow'), 'sort' => 5 ), - ); - } - + ); + } } - -class ExistingWorkflowException extends Exception {}; diff --git a/code/tasks/WorkflowReminderTask.php b/code/tasks/WorkflowReminderTask.php index 275d2d34..cb26f0ef 100644 --- a/code/tasks/WorkflowReminderTask.php +++ b/code/tasks/WorkflowReminderTask.php @@ -1,6 +1,13 @@ count()) { // Don't attempt the filter if no instances -- prevents a crash - $active = WorkflowInstance::get() - ->innerJoin('WorkflowDefinition', '"DefinitionID" = "WorkflowDefinition"."ID"') - ->filter(array('WorkflowStatus' => array('Active', 'Paused'), 'RemindDays:GreaterThan' => '0')); - $active->filter(array('RemindDays:GreaterThan' => '0')); - if ($active) foreach ($active as $instance) { - $edited = strtotime($instance->LastEdited); - $days = $instance->Definition()->RemindDays; - - if ($edited + $days * 3600 * 24 > time()) { - continue; - } - - $email = new Email(); - $bcc = ''; - $members = $instance->getAssignedMembers(); - $target = $instance->getTarget(); - - if (!$members || !count($members)) continue; - - $email->setSubject("Workflow Reminder: $instance->Title"); - $email->setBcc(implode(', ', $members->column('Email'))); - $email->setTemplate('WorkflowReminderEmail'); - $email->populateTemplate(array( - 'Instance' => $instance, - 'Link' => $target instanceof SiteTree ? "admin/show/$target->ID" : '', - 'Diff' => $instance->getTargetDiff() - )); +class WorkflowReminderTask extends BuildTask +{ + protected $title = 'Workflow Reminder Task'; + protected $description = 'Sends out workflow reminder emails to stale workflow instances'; + + private static $segment = 'WorkflowReminderTask'; + + public function run($request) + { + $sent = 0; + if (WorkflowInstance::get()->count()) { // Don't attempt the filter if no instances -- prevents a crash + $active = WorkflowInstance::get() + ->innerJoin(WorkflowDefinition::class, '"DefinitionID" = "WorkflowDefinition"."ID"') + ->filter(array('WorkflowStatus' => array('Active', 'Paused'), 'RemindDays:GreaterThan' => '0')) + ->filter(array('RemindDays:GreaterThan' => '0')); + + if ($active) { + foreach ($active as $instance) { + $edited = strtotime($instance->LastEdited); + $days = $instance->Definition()->RemindDays; - $email->send(); - $sent++; + if ($edited + $days * 3600 * 24 > DBDatetime::now()->getTimestamp()) { + continue; + } + + $email = new Email(); + $bcc = ''; + $members = $instance->getAssignedMembers(); + $target = $instance->getTarget(); + + if (!$members || !count($members)) { + continue; + } + + $email->setSubject("Workflow Reminder: $instance->Title"); + $email->setBcc(implode(', ', $members->column(Email::class))); + $email->setHTMLTemplate('WorkflowReminderEmail'); + $email->populateTemplate(array( + 'Instance' => $instance, + 'Link' => $target instanceof SiteTree ? "admin/show/$target->ID" : '', + 'Diff' => $instance->getTargetDiff() + )); - $instance->LastEdited = time(); - $instance->write(); - } - } - echo "Sent $sent workflow reminder emails.\n"; - } + $email->send(); + $sent++; + $instance->LastEdited = DBDatetime::now()->getTimestamp(); + $instance->write(); + } + } + } + echo "Sent $sent workflow reminder emails.\n"; + } } diff --git a/code/templates/WorkflowTemplate.php b/code/templates/WorkflowTemplate.php index f39aaeaa..9df128ab 100644 --- a/code/templates/WorkflowTemplate.php +++ b/code/templates/WorkflowTemplate.php @@ -1,28 +1,37 @@ class name - * 'transitions' => array( - * 'transition name' => 'target step', - * 'next name' => 'other step' - * ) - * ), - * 'Next Step' = array( - * - * ), + * 'Step Name' = array( + * 'type' => class name + * 'transitions' => array( + * 'transition name' => 'target step', + * 'next name' => 'other step' + * ) + * ), + * 'Next Step' = array( + * + * ), * ) - * + * * This can be defined in config yml as follows - * - * Injector: + * + * SilverStripe\Core\Injector\Injector: * SimpleReviewApprove: - * class: WorkflowTemplate + * class: Symbiote\AdvancedWorkflow\Templates\WorkflowTemplate * constructor: * - Review and Approve * - Description of the workflow template @@ -31,7 +40,7 @@ * structure: * Apply for approval: * type: AssignUsersToWorkflowAction - * transitions: + * transitions: * notify: Notify users * Notify users: * type: NotifyUsersWorkflowAction @@ -46,375 +55,387 @@ * type: PublishItemWorkflowAction * Reject: * type: CancelWorkflowAction - * WorkflowService: + * Symbiote\AdvancedWorkflow\Services\WorkflowService: * properties: * templates: * - %$SimpleReviewApprove - * + * * When updating a template, there's a few things that can be done to assist * the system when changing things around - * - * 1. Update the 'version' number - * + * + * 1. Update the 'version' number + * * @author marcus@symbiote.com.au * @license BSD License http://silverstripe.org/bsd-license/ */ -class WorkflowTemplate { - protected $name; - protected $description; - protected $version; - protected $remindDays; - protected $sort; - - /** - * An array representation of the structure of this workflow template - * - * @var array - */ - protected $structure; - - public function __construct($name, $description = '', $version = '0.0', $remindDays = 0, $sort = 0) { - $this->name = $name; - $this->description = $description; - $this->version = $version; - $this->remindDays = $remindDays; - $this->sort = $sort; - } - - public function getName() { - return $this->name; - } - - public function getVersion() { - return $this->version; - } - - public function getDescription() { - return $this->description; - } - - public function getRemindDays() { - return $this->remindDays; - } - - public function getSort() { - return $this->sort; - } - - /** - * Set the structure for this template - * - * @param array $structure - */ - public function setStructure($structure) { - $this->structure = $structure; - } - - /** - * Creates the relevant data objects for this structure, returning an array - * of actions in the order they've been created - * - * @param WorkflowDefinition $definitino - * An optional workflow definition to bind the actions into - * @return array - */ - public function createRelations($definition = null) { - $actions = array(); - $transitions = new ArrayObject(); - $sort = 1; - foreach ($this->structure as $relationName => $relationTemplate) { - - $isAction = isset($relationTemplate['type']); - $isUsers = ($relationName == 'users'); - $isGroups = ($relationName == 'groups'); - - // Process actions on WorkflowDefinition from the template - if($isAction) { - $action = $this->createAction($relationName, $relationTemplate, $definition); - // add a sort value in! - $action->Sort = $sort++; - $action->write(); - - $actions[$relationName] = $action; - - $newTransitions = $this->updateActionTransitions($relationTemplate, $action); - foreach ($newTransitions as $t) { - $transitions->append($t); - } - } - // Process users on WorkflowDefinition from the template - if($isUsers) { - $this->createUsers($relationTemplate, $definition); - } - // Process groups on WorkflowDefinition from the template - if($isGroups) { - $this->createGroups($relationTemplate, $definition); - } - } - - foreach ($transitions as $transition) { - if (isset($actions[$transition->Target])) { - $transition->NextActionID = $actions[$transition->Target]->ID; - } - $transition->write(); - } - - return $actions; - } - - /** - * Create a workflow action based on a template - * - * @param string $name - * @param array $template - * @param WorkflowDefinition $definition - * @return WorkflowAction - */ - protected function createAction($name, $actionTemplate, WorkflowDefinition $definition = null) { - $type = $actionTemplate['type']; - if (!$type || !class_exists($type)) { - throw new Exception(_t('WorkflowTemplate.INVALID_TEMPLATE_ACTION', 'Invalid action class specified in template')); - } - - $action = $type::create(); - - $action->Title = $name; - - if (isset($actionTemplate['properties']) && is_array($actionTemplate['properties'])) { - foreach ($actionTemplate['properties'] as $prop => $val) { - $action->$prop = $val; - } - } - - // Deal with User + Group many_many relations on an action - $this->addManyManyToObject($action, $actionTemplate); - - if ($definition) { - $action->WorkflowDefID = $definition->ID; - } - - $action->write(); - - return $action; - } - - /** - * Create a WorkflowDefinition->Users relation based on template data. But only if the related groups from the - * export, can be foud in the target environment's DB. - * - * Note: The template gives us a Member Email to work with rather than an ID as it's arguably - * more likely that Member Emails will be the same between environments than their IDs. - * - * @param array $users - * @param WorkflowDefinition $definition - * @param boolean $clear - * @return void - */ - protected function createUsers($users, WorkflowDefinition $definition, $clear = false) { - // Create the necessary relation in WorkflowDefinition_Users - $source = array('users' => $users); - $this->addManyManyToObject($definition, $source, $clear); - } - - /** - * Create a WorkflowDefinition->Groups relation based on template data, But only if the related groups from the - * export, can be foud in the target environment's DB. - * - * Note: The template gives us a Group Title to work with rther than an ID as it's arguably - * more likely that Group titles will be the same between environments than their IDs. - * - * @param array $groups - * @param WorkflowDefinition $definition - * @param boolean $clear - * @return void - */ - protected function createGroups($groups, WorkflowDefinition $definition, $clear = false) { - // Create the necessary relation in WorkflowDefinition_Groups - $source = array('groups' => $groups); - $this->addManyManyToObject($definition, $source, $clear); - } - - /** - * Update the transitions for a given action - * - * @param array $actionTemplate - * @param WorkflowAction $action - * - * @return array - */ - protected function updateActionTransitions($actionTemplate, $action) { - $transitions = array(); - if (isset($actionTemplate['transitions']) && is_array($actionTemplate['transitions'])) { - - $existing = $action->Transitions(); - $transitionMap = array(); - foreach ($existing as $transition) { - $transitionMap[$transition->Title] = $transition; - } - - foreach ($actionTemplate['transitions'] as $transitionName => $transitionTemplate) { - $target = $transitionTemplate; - if (is_array($transitionTemplate)) { - $to = array_keys($transitionTemplate); - $transitionName = $to[0]; - $target = $transitionTemplate[$transitionName]; - } - - if (isset($transitionMap[$transitionName])) { - $transition = $transitionMap[$transitionName]; - } else { - $transition = WorkflowTransition::create(); - } - - // Add Member and Group relations to this Transition - $this->addManyManyToObject($transition, $transitionTemplate); - - $transition->Title = $transitionName; - $transition->ActionID = $action->ID; - // we don't have the NextAction yet other than the target name, so we store that against - // the transition and do a second pass later on to match things up - $transition->Target = $target; - $transitions[] = $transition; - } - } - - return $transitions; - } - - /** - * Update a workflow definition - * - * @param WorkflowDefinition $definition - * The definition to update - */ - public function updateDefinition(WorkflowDefinition $definition) { - $existingActions = array(); - - $existing = $definition->Actions()->column('Title'); - $structure = array_keys($this->structure); - - $removeNames = array_diff($existing, $structure); - - foreach ($definition->Actions() as $action) { - if (in_array($action->Title, $removeNames)) { - $action->delete(); - continue; - } - $existingActions[$action->Title] = $action; - } - - $actions = array(); - $transitions = new ArrayObject; - $sort = 1; - // now, go through the structure and create/realign things - foreach ($this->structure as $relationName => $relationTemplate) { - - $isAction = isset($relationTemplate['type']); - $isUsers = ($relationName == 'users'); - $isGroups = ($relationName == 'groups'); - - if($isAction) { - $action = null; - if (isset($existingActions[$relationName])) { - $action = $existingActions[$relationName]; - } else { - $action = $this->createAction($relationName, $relationTemplate, $definition, $transitions); - } - - // add a sort value in! - $action->Sort = $sort++; - $action->write(); - - $actions[$relationName] = $action; - - $newTransitions = $this->updateActionTransitions($relationTemplate, $action); - foreach ($newTransitions as $t) { - $transitions->append($t); - } - } - // Process users on WorkflowDefinition from the template - if($isUsers) { - $this->createUsers($relationTemplate, $definition, true); - } - // Process groups on WorkflowDefinition from the template - if($isGroups) { - $this->createGroups($relationTemplate, $definition, true); - } - } - - foreach ($transitions as $transition) { - if (isset($actions[$transition->Target])) { - $transition->NextActionID = $actions[$transition->Target]->ID; - } - $transition->write(); - } - - // Set the version and do the write at the end so that we don't trigger an infinite loop!! - $definition->Description = $this->getDescription(); - $definition->TemplateVersion = $this->getVersion(); - $definition->RemindDays = $this->getRemindDays(); - $definition->Sort = $this->getSort(); - $definition->write(); - } - - /** - * Given an object, first check it has a ManyMany relation on it and add() Member and Group relations as required. - * - * @param Object $object (e.g. WorkflowDefinition, WorkflowAction, WorkflowTransition) - * @param array $source Usually data taken from a YAML template - * @param boolean $clear Lose/keep Group/Member relations on $object (useful for reloading/refreshing definition) - * @return void - */ - protected function addManyManyToObject($object, $source, $clear = false) { - // Check incoming - if(!is_object($object) || !is_array($source)) { - return; - } - - // Only some target class variants actually have Group/User relations - $hasUsers = false; - $hasGroups = false; - if($manyMany = $object->stat('many_many')) { - if(in_array('SilverStripe\\Security\\Member', $manyMany)) { - $hasUsers = true; - $userRelationName = array_keys($manyMany); - } - if(in_array('SilverStripe\\Security\\Group', $manyMany)) { - $hasGroups = true; - $groupRelationName = array_keys($manyMany); - } - } - - // Deal with User relations on target object - if($hasUsers) { - if($clear) { - $relName = $userRelationName[0]; - $object->$relName()->removeAll(); - } - if(isset($source['users']) && is_array($source['users'])) { - foreach ($source['users'] as $user) { - $email = Convert::raw2sql($user['email']); - if($_user = DataObject::get_one('SilverStripe\\Security\\Member', "Email = '".$email."'")) { - $object->Users()->add($_user); - } - } - } - } - - // Deal with Group relations on target object - if($hasGroups) { - if($clear) { - $relName = $groupRelationName[0]; - $object->$relName()->removeAll(); - } - if(isset($source['groups']) && is_array($source['groups'])) { - foreach ($source['groups'] as $group) { - $title = Convert::raw2sql($group['title']); - if($_group = DataObject::get_one('SilverStripe\\Security\\Group', "Title = '".$title."'")) { - $object->Groups()->add($_group); - } - } - } - } - } +class WorkflowTemplate +{ + protected $name; + protected $description; + protected $version; + protected $remindDays; + protected $sort; + + /** + * An array representation of the structure of this workflow template + * + * @var array + */ + protected $structure; + + public function __construct($name, $description = '', $version = '0.0', $remindDays = 0, $sort = 0) + { + $this->name = $name; + $this->description = $description; + $this->version = $version; + $this->remindDays = $remindDays; + $this->sort = $sort; + } + + public function getName() + { + return $this->name; + } + + public function getVersion() + { + return $this->version; + } + + public function getDescription() + { + return $this->description; + } + + public function getRemindDays() + { + return $this->remindDays; + } + + public function getSort() + { + return $this->sort; + } + + /** + * Set the structure for this template + * + * @param array $structure + */ + public function setStructure($structure) + { + $this->structure = $structure; + } + + /** + * Creates the relevant data objects for this structure, returning an array + * of actions in the order they've been created + * + * @param WorkflowDefinition $definitino + * An optional workflow definition to bind the actions into + * @return array + */ + public function createRelations($definition = null) + { + $actions = array(); + $transitions = new ArrayObject(); + $sort = 1; + foreach ($this->structure as $relationName => $relationTemplate) { + $isAction = isset($relationTemplate['type']); + $isUsers = ($relationName == 'users'); + $isGroups = ($relationName == 'groups'); + + // Process actions on WorkflowDefinition from the template + if ($isAction) { + $action = $this->createAction($relationName, $relationTemplate, $definition); + // add a sort value in! + $action->Sort = $sort++; + $action->write(); + + $actions[$relationName] = $action; + + $newTransitions = $this->updateActionTransitions($relationTemplate, $action); + foreach ($newTransitions as $t) { + $transitions->append($t); + } + } + // Process users on WorkflowDefinition from the template + if ($isUsers) { + $this->createUsers($relationTemplate, $definition); + } + // Process groups on WorkflowDefinition from the template + if ($isGroups) { + $this->createGroups($relationTemplate, $definition); + } + } + + foreach ($transitions as $transition) { + if (isset($actions[$transition->Target])) { + $transition->NextActionID = $actions[$transition->Target]->ID; + } + $transition->write(); + } + + return $actions; + } + + /** + * Create a workflow action based on a template + * + * @param string $name + * @param array $template + * @param WorkflowDefinition $definition + * @return WorkflowAction + * @throws Exception + */ + protected function createAction($name, $actionTemplate, WorkflowDefinition $definition = null) + { + $type = $actionTemplate['type']; + if (!$type || !class_exists($type)) { + throw new Exception(_t('WorkflowTemplate.INVALID_TEMPLATE_ACTION', 'Invalid action class specified in template')); + } + + $action = $type::create(); + + $action->Title = $name; + + if (isset($actionTemplate['properties']) && is_array($actionTemplate['properties'])) { + foreach ($actionTemplate['properties'] as $prop => $val) { + $action->$prop = $val; + } + } + + // Deal with User + Group many_many relations on an action + $this->addManyManyToObject($action, $actionTemplate); + + if ($definition) { + $action->WorkflowDefID = $definition->ID; + } + + $action->write(); + + return $action; + } + + /** + * Create a WorkflowDefinition->Users relation based on template data. But only if the related groups from the + * export, can be foud in the target environment's DB. + * + * Note: The template gives us a Member Email to work with rather than an ID as it's arguably + * more likely that Member Emails will be the same between environments than their IDs. + * + * @param array $users + * @param WorkflowDefinition $definition + * @param boolean $clear + * @return void + */ + protected function createUsers($users, WorkflowDefinition $definition, $clear = false) + { + // Create the necessary relation in WorkflowDefinition_Users + $source = array('users' => $users); + $this->addManyManyToObject($definition, $source, $clear); + } + + /** + * Create a WorkflowDefinition->Groups relation based on template data, But only if the related groups from the + * export, can be foud in the target environment's DB. + * + * Note: The template gives us a Group Title to work with rther than an ID as it's arguably + * more likely that Group titles will be the same between environments than their IDs. + * + * @param array $groups + * @param WorkflowDefinition $definition + * @param boolean $clear + * @return void + */ + protected function createGroups($groups, WorkflowDefinition $definition, $clear = false) + { + // Create the necessary relation in WorkflowDefinition_Groups + $source = array('groups' => $groups); + $this->addManyManyToObject($definition, $source, $clear); + } + + /** + * Update the transitions for a given action + * + * @param array $actionTemplate + * @param WorkflowAction $action + * + * @return array + */ + protected function updateActionTransitions($actionTemplate, $action) + { + $transitions = array(); + if (isset($actionTemplate['transitions']) && is_array($actionTemplate['transitions'])) { + $existing = $action->Transitions(); + $transitionMap = array(); + foreach ($existing as $transition) { + $transitionMap[$transition->Title] = $transition; + } + + foreach ($actionTemplate['transitions'] as $transitionName => $transitionTemplate) { + $target = $transitionTemplate; + if (is_array($transitionTemplate)) { + $to = array_keys($transitionTemplate); + $transitionName = $to[0]; + $target = $transitionTemplate[$transitionName]; + } + + if (isset($transitionMap[$transitionName])) { + $transition = $transitionMap[$transitionName]; + } else { + $transition = WorkflowTransition::create(); + } + + // Add Member and Group relations to this Transition + $this->addManyManyToObject($transition, $transitionTemplate); + + $transition->Title = $transitionName; + $transition->ActionID = $action->ID; + // we don't have the NextAction yet other than the target name, so we store that against + // the transition and do a second pass later on to match things up + $transition->Target = $target; + $transitions[] = $transition; + } + } + + return $transitions; + } + + /** + * Update a workflow definition + * + * @param WorkflowDefinition $definition The definition to update + */ + public function updateDefinition(WorkflowDefinition $definition) + { + $existingActions = array(); + + $existing = $definition->Actions()->column('Title'); + $structure = array_keys($this->structure); + + $removeNames = array_diff($existing, $structure); + + foreach ($definition->Actions() as $action) { + if (in_array($action->Title, $removeNames)) { + $action->delete(); + continue; + } + $existingActions[$action->Title] = $action; + } + + $actions = array(); + $transitions = new ArrayObject; + $sort = 1; + // now, go through the structure and create/realign things + foreach ($this->structure as $relationName => $relationTemplate) { + $isAction = isset($relationTemplate['type']); + $isUsers = ($relationName == 'users'); + $isGroups = ($relationName == 'groups'); + + if ($isAction) { + $action = null; + if (isset($existingActions[$relationName])) { + $action = $existingActions[$relationName]; + } else { + $action = $this->createAction($relationName, $relationTemplate, $definition, $transitions); + } + + // add a sort value in! + $action->Sort = $sort++; + $action->write(); + + $actions[$relationName] = $action; + + $newTransitions = $this->updateActionTransitions($relationTemplate, $action); + foreach ($newTransitions as $t) { + $transitions->append($t); + } + } + // Process users on WorkflowDefinition from the template + if ($isUsers) { + $this->createUsers($relationTemplate, $definition, true); + } + // Process groups on WorkflowDefinition from the template + if ($isGroups) { + $this->createGroups($relationTemplate, $definition, true); + } + } + + foreach ($transitions as $transition) { + if (isset($actions[$transition->Target])) { + $transition->NextActionID = $actions[$transition->Target]->ID; + } + $transition->write(); + } + + // Set the version and do the write at the end so that we don't trigger an infinite loop!! + $definition->Description = $this->getDescription(); + $definition->TemplateVersion = $this->getVersion(); + $definition->RemindDays = $this->getRemindDays(); + $definition->Sort = $this->getSort(); + $definition->write(); + } + + /** + * Given an object, first check it has a ManyMany relation on it and add() Member and Group relations as required. + * + * @param Object $object (e.g. WorkflowDefinition, WorkflowAction, WorkflowTransition) + * @param array $source Usually data taken from a YAML template + * @param boolean $clear Lose/keep Group/Member relations on $object (useful for reloading/refreshing definition) + * @return void + */ + protected function addManyManyToObject($object, $source, $clear = false) + { + // Check incoming + if (!is_object($object) || !is_array($source)) { + return; + } + + // Only some target class variants actually have Group/User relations + $hasUsers = false; + $hasGroups = false; + if ($manyMany = $object->stat('many_many')) { + if (in_array(Member::class, $manyMany)) { + $hasUsers = true; + $userRelationName = array_keys($manyMany); + } + if (in_array(Group::class, $manyMany)) { + $hasGroups = true; + $groupRelationName = array_keys($manyMany); + } + } + + // Deal with User relations on target object + if ($hasUsers) { + if ($clear) { + $relName = $userRelationName[0]; + $object->$relName()->removeAll(); + } + if (isset($source['users']) && is_array($source['users'])) { + foreach ($source['users'] as $user) { + $email = Convert::raw2sql($user['email']); + if ($_user = DataObject::get_one(Member::class, "Email = '".$email."'")) { + $object->Users()->add($_user); + } + } + } + } + + // Deal with Group relations on target object + if ($hasGroups) { + if ($clear) { + $relName = $groupRelationName[0]; + $object->$relName()->removeAll(); + } + if (isset($source['groups']) && is_array($source['groups'])) { + foreach ($source['groups'] as $group) { + $title = Convert::raw2sql($group['title']); + if ($_group = DataObject::get_one(Group::class, "Title = '".$title."'")) { + $object->Groups()->add($_group); + } + } + } + } + } } diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..69cb7601 --- /dev/null +++ b/codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/composer.json b/composer.json index 0e324c22..4501ef96 100644 --- a/composer.json +++ b/composer.json @@ -1,31 +1,48 @@ { - "name": "symbiote/silverstripe-advancedworkflow", - "description": "Adds configurable workflow support to the CMS, with a GUI for creating custom workflow definitions.", - "type": "silverstripe-module", - "keywords": ["silverstripe", "advancedworkflow", "workflow"], - "license": "BSD-3-Clause", - "authors": [ - { - "name": "Marcus Nyeholt", - "email": "marcus@symbiote.com.au" - }, - { - "name": "Andrew Short", - "email": "andrewjshort@gmail.com" - } - ], - "require": - { + "name": "symbiote/silverstripe-advancedworkflow", + "description": "Adds configurable workflow support to the CMS, with a GUI for creating custom workflow definitions.", + "type": "silverstripe-module", + "keywords": ["silverstripe", "advancedworkflow", "workflow"], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Marcus Nyeholt", + "email": "marcus@symbiote.com.au" + }, + { + "name": "Andrew Short", + "email": "andrewjshort@gmail.com" + } + ], + "require": { "silverstripe/cms": "^4", - "silverstripe/framework": "^4" - }, - "extra": { - "installer-name": "advancedworkflow", - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "replace": { - "silverstripe/advancedworkflow": "self.version" - } + "silverstripe/framework": "^4", + "silverstripe/admin": "^1", + "silverstripe/versioned": "^1", + "symfony/yaml": "~3.2" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "squizlabs/php_codesniffer": "^3.0" + }, + "extra": { + "installer-name": "advancedworkflow", + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "suggest": { + "symbiote/silverstripe-queuedjobs": "Allow automated workflow transitions with queued system jobs" + }, + "autoload": { + "psr-4": { + "Symbiote\\AdvandedWorkflow\\": "code/", + "Symbiote\\AdvandedWorkflow\\Tests\\": "tests/" + } + }, + "replace": { + "silverstripe/advancedworkflow": "self.version" + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..d7fa2ad4 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,14 @@ + + + tests/ + + + + + code/ + + tests/ + + + + diff --git a/tests/WorkflowEmbargoExpiry.yml b/tests/WorkflowEmbargoExpiry.yml index eed818a5..345932e2 100644 --- a/tests/WorkflowEmbargoExpiry.yml +++ b/tests/WorkflowEmbargoExpiry.yml @@ -1,20 +1,20 @@ -WorkflowDefinition: +Symbiote\AdvancedWorkflow\DataObjects\WorkflowDefinition: requestPublication: Title: 'Request Publish' approvePublication: Title: 'Approve Publish' -SimpleApprovalWorkflowAction: +Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction: requestPublication: Title: 'Approve' - WorkflowDefID: =>WorkflowDefinition.requestPublication + WorkflowDefID: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowDefinition.requestPublication -PublishItemWorkflowAction: +Symbiote\AdvancedWorkflow\Actions\PublishItemWorkflowAction: approvePublication: Title: 'Publish' - WorkflowDefID: =>WorkflowDefinition.approvePublication + WorkflowDefID: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowDefinition.approvePublication -SiteTree: +SilverStripe\CMS\Model\SiteTree: emptyEmbargoExpiry: Title: 'Empty embargo and expiry' pastEmbargo: diff --git a/tests/WorkflowEmbargoExpiryTest.php b/tests/WorkflowEmbargoExpiryTest.php index 4ea8526b..075b7e21 100644 --- a/tests/WorkflowEmbargoExpiryTest.php +++ b/tests/WorkflowEmbargoExpiryTest.php @@ -1,101 +1,109 @@ markTestSkipped("This test requires queuedjobs"); } - } - public function tearDown() + // This doesn't play nicely with PHPUnit + Config::modify()->set(QueuedJobService::class, 'use_shutdown_function', false); + } + + protected function tearDown() { DBDatetime::clear_mock_now(); - parent::tearDown(); - } - - /** - * @var array - */ - protected $requiredExtensions = array( - 'SiteTree' => array( - 'WorkflowEmbargoExpiryExtension', - 'SilverStripe\\ORM\\Versioning\\Versioned', - ) - ); - - /** - * @var array - */ - protected $illegalExtensions = array( - 'SiteTree' => array( - "Translatable", - ) - ); - - public function __construct() - { - if (!class_exists('AbstractQueuedJob')) { - $this->skipTest = true; - } - parent::__construct(); - } - - /** - * Start a workflow for a page, - * this will set it into a state where a workflow is currently being processes - * - * @param DataObject $obj - * @return DataObject - */ - private function startWorkflow($obj) - { - $workflow = $this->objFromFixture('WorkflowDefinition', 'requestPublication'); - $obj->WorkflowDefinitionID = $workflow->ID; - $obj->write(); - - $svc = singleton('WorkflowService'); - $svc->startWorkflow($obj, $obj->WorkflowDefinitionID); - return $obj; - } - - /** - * Start and finish a workflow which will publish the page immediately basically. - * - * @param DataObject $obj - * @return DataObject - */ - private function finishWorkflow($obj) - { - $workflow = $this->objFromFixture('WorkflowDefinition', 'approvePublication'); - $obj->WorkflowDefinitionID = $workflow->ID; - $obj->write(); - - $svc = singleton('WorkflowService'); - $svc->startWorkflow($obj, $obj->WorkflowDefinitionID); - - $obj = DataObject::get_by_id($obj->ClassName, $obj->ID); - return $obj; - } + parent::tearDown(); + } + + /** + * @var array + */ + protected static $required_extensions = array( + SiteTree::class => array( + WorkflowEmbargoExpiryExtension::class, + Versioned::class, + ), + ); + + /** + * @var array + */ + protected static $illegal_extensions = array( + SiteTree::class => array( + Translatable::class, + SiteTreeSubsites::class, + ), + ); + + /** + * Start a workflow for a page, + * this will set it into a state where a workflow is currently being processes + * + * @param DataObject $obj + * @return DataObject + */ + private function startWorkflow($obj) + { + $workflow = $this->objFromFixture(WorkflowDefinition::class, 'requestPublication'); + $obj->WorkflowDefinitionID = $workflow->ID; + $obj->write(); + + $svc = singleton(WorkflowService::class); + $svc->startWorkflow($obj, $obj->WorkflowDefinitionID); + return $obj; + } + + /** + * Start and finish a workflow which will publish the page immediately basically. + * + * @param DataObject $obj + * @return DataObject + */ + private function finishWorkflow($obj) + { + $workflow = $this->objFromFixture(WorkflowDefinition::class, 'approvePublication'); + $obj->WorkflowDefinitionID = $workflow->ID; + $obj->write(); + + $svc = singleton(WorkflowService::class); + $svc->startWorkflow($obj, $obj->WorkflowDefinitionID); + + $obj = DataObject::get($obj->ClassName)->byID($obj->ID); + return $obj; + } /** * Retrieves the live version for an object @@ -107,7 +115,7 @@ private function getLive($obj) { $oldMode = Versioned::get_reading_mode(); Versioned::set_reading_mode(Versioned::LIVE); - $live = DataObject::get_by_id($obj->ClassName, $obj->ID); + $live = DataObject::get($obj->ClassName)->byID($obj->ID); Versioned::set_reading_mode($oldMode); return $live; @@ -120,7 +128,7 @@ private function getLive($obj) */ public function testEmptyEmbargoExpiry() { - $page = $this->objFromFixture('SiteTree', 'emptyEmbargoExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'emptyEmbargoExpiry'); $page->Content = 'Content to go live'; $live = $this->getLive($page); @@ -145,7 +153,7 @@ public function testEmptyEmbargoExpiry() */ public function testPastEmbargo() { - $page = $this->objFromFixture('SiteTree', 'pastEmbargo'); + $page = $this->objFromFixture(SiteTree::class, 'pastEmbargo'); $page = $this->finishWorkflow($page); @@ -153,7 +161,6 @@ public function testPastEmbargo() $this->assertEquals(0, $page->UnPublishJobID); $publish = strtotime($page->PublishJob()->StartAfter); - $this->assertFalse($publish); } @@ -164,7 +171,7 @@ public function testPastEmbargo() */ public function testPastExpiry() { - $page = $this->objFromFixture('SiteTree', 'pastExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'pastExpiry'); $page = $this->finishWorkflow($page); @@ -183,7 +190,7 @@ public function testPastExpiry() */ public function testPastEmbargoExpiry() { - $page = $this->objFromFixture('SiteTree', 'pastEmbargoExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'pastEmbargoExpiry'); $page = $this->finishWorkflow($page); @@ -202,7 +209,7 @@ public function testPastEmbargoExpiry() */ public function testPastEmbargoFutureExpiry() { - $page = $this->objFromFixture('SiteTree', 'pastEmbargoFutureExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'pastEmbargoFutureExpiry'); $page = $this->finishWorkflow($page); @@ -223,7 +230,7 @@ public function testPastEmbargoFutureExpiry() */ public function testFutureEmbargoExpiry() { - $page = $this->objFromFixture('SiteTree', 'futureEmbargoExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'futureEmbargoExpiry'); $page = $this->finishWorkflow($page); @@ -244,7 +251,7 @@ public function testFutureEmbargoExpiry() */ public function testPastEmbargoAfterExpiry() { - $page = $this->objFromFixture('SiteTree', 'pastEmbargoAfterExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'pastEmbargoAfterExpiry'); $page = $this->finishWorkflow($page); @@ -259,7 +266,7 @@ public function testPastEmbargoAfterExpiry() */ public function testFutureEmbargoAfterExpiry() { - $page = $this->objFromFixture('SiteTree', 'futureEmbargoAfterExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'futureEmbargoAfterExpiry'); $page = $this->finishWorkflow($page); @@ -274,7 +281,7 @@ public function testFutureEmbargoAfterExpiry() */ public function testPastSameEmbargoExpiry() { - $page = $this->objFromFixture('SiteTree', 'pastSameEmbargoExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'pastSameEmbargoExpiry'); $page = $this->finishWorkflow($page); @@ -289,7 +296,7 @@ public function testPastSameEmbargoExpiry() */ public function testFutureSameEmbargoExpiry() { - $page = $this->objFromFixture('SiteTree', 'futureSameEmbargoExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'futureSameEmbargoExpiry'); $page = $this->finishWorkflow($page); @@ -302,23 +309,23 @@ public function testFutureSameEmbargoExpiry() * * The existing queued jobs should be cleared */ - public function testDesiredRemovesJobs() + public function testDesiredRemovesJobs() { - $page = $this->objFromFixture('SiteTree', 'futureEmbargoExpiry'); + $page = $this->objFromFixture(SiteTree::class, 'futureEmbargoExpiry'); $page = $this->finishWorkflow($page); - $this->assertNotEquals(0, $page->PublishJobID); - $this->assertNotEquals(0, $page->UnPublishJobID); + $this->assertNotEquals(0, $page->PublishJobID); + $this->assertNotEquals(0, $page->UnPublishJobID); - $page->DesiredPublishDate = '2020-02-01 00:00:00'; - $page->DesiredUnPublishDate = '2020-02-01 02:00:00'; + $page->DesiredPublishDate = '2020-02-01 00:00:00'; + $page->DesiredUnPublishDate = '2020-02-01 02:00:00'; - $page->write(); + $page->write(); - $this->assertEquals(0, $page->PublishJobID); - $this->assertEquals(0, $page->UnPublishJobID); - } + $this->assertEquals(0, $page->PublishJobID); + $this->assertEquals(0, $page->UnPublishJobID); + } /** * Tests that checking for publishing scheduled state is working @@ -398,69 +405,71 @@ public function testStatusFlags() $this->assertNotContains('expiry', array_keys($flags)); } - /** - * Test workflow definition "Can disable edits during embargo" - * Make sure page cannot be edited when an embargo is in place - */ - public function testCanEditConfig() + /** + * Test workflow definition "Can disable edits during embargo" + * Make sure page cannot be edited when an embargo is in place + */ + public function testCanEditConfig() { + $this->logOut(); - $page = SiteTree::create(); - $page->Title = 'My page'; - $page->PublishOnDate = '2010-01-01 00:00:00'; - $page->write(); - - $memberID = $this->logInWithPermission('SITETREE_EDIT_ALL'); - $this->assertTrue($page->canEdit(), 'Can edit page without embargo and no permission'); + $page = SiteTree::create(); + $page->Title = 'My page'; + $page->PublishOnDate = '2010-01-01 00:00:00'; + $page->write(); - $page->PublishOnDate = '2020-01-01 00:00:00'; - $page->write(); - $this->assertFalse($page->canEdit(), 'Cannot edit page with embargo and no permission'); + $memberID = $this->logInWithPermission('SITETREE_EDIT_ALL'); + $this->assertTrue($page->canEdit(), 'Can edit page without embargo and no permission'); - $this->logOut(); - $memberID = $this->logInWithPermission('ADMIN'); - $this->assertTrue($page->canEdit(), 'Can edit page with embargo as Admin'); + $page->PublishOnDate = '2020-01-01 00:00:00'; + $page->AllowEmbargoedEditing = false; + $page->write(); + $this->assertFalse($page->canEdit(), 'Cannot edit page with embargo and no permission'); - $this->logOut(); - $memberID = $this->logInWithPermission(array('SITETREE_EDIT_ALL', 'EDIT_EMBARGOED_WORKFLOW')); - $this->assertTrue($page->canEdit(), 'Can edit page with embargo and permission'); + $this->logOut(); + $memberID = $this->logInWithPermission('ADMIN'); + $this->assertTrue($page->canEdit(), 'Can edit page with embargo as Admin'); - $page->PublishOnDate = '2010-01-01 00:00:00'; - $page->write(); - $this->assertTrue($page->canEdit(), 'Can edit page without embargo and permission'); + $this->logOut(); + $memberID = $this->logInWithPermission(array('SITETREE_EDIT_ALL', 'EDIT_EMBARGOED_WORKFLOW')); + $this->assertTrue($page->canEdit(), 'Can edit page with embargo and permission'); - } + $page->PublishOnDate = '2010-01-01 00:00:00'; + $page->write(); + $this->assertTrue($page->canEdit(), 'Can edit page without embargo and permission'); + } - protected function createDefinition() + protected function createDefinition() { - $definition = new WorkflowDefinition(); - $definition->Title = 'Dummy Workflow Definition'; - $definition->write(); - - $stepOne = new WorkflowAction(); - $stepOne->Title = 'Step One'; - $stepOne->WorkflowDefID = $definition->ID; - $stepOne->write(); - - $stepTwo = new WorkflowAction(); - $stepTwo->Title = 'Step Two'; - $stepTwo->WorkflowDefID = $definition->ID; - $stepTwo->write(); - - $transitionOne = new WorkflowTransition(); - $transitionOne->Title = 'Step One T1'; - $transitionOne->ActionID = $stepOne->ID; - $transitionOne->NextActionID = $stepTwo->ID; - $transitionOne->write(); - - return $definition; - } + $definition = new WorkflowDefinition(); + $definition->Title = 'Dummy Workflow Definition'; + $definition->write(); + + $stepOne = new WorkflowAction(); + $stepOne->Title = 'Step One'; + $stepOne->WorkflowDefID = $definition->ID; + $stepOne->write(); + + $stepTwo = new WorkflowAction(); + $stepTwo->Title = 'Step Two'; + $stepTwo->WorkflowDefID = $definition->ID; + $stepTwo->write(); + + $transitionOne = new WorkflowTransition(); + $transitionOne->Title = 'Step One T1'; + $transitionOne->ActionID = $stepOne->ID; + $transitionOne->NextActionID = $stepTwo->ID; + $transitionOne->write(); + + return $definition; + } /** * Make sure that publish and unpublish dates are not carried over to the duplicates. */ - public function testDuplicateRemoveEmbargoExpiry() { - $page = $this->objFromFixture('SiteTree', 'futureEmbargoExpiry'); + public function testDuplicateRemoveEmbargoExpiry() + { + $page = $this->objFromFixture(SiteTree::class, 'futureEmbargoExpiry'); // fake publish jobs $page = $this->finishWorkflow($page); @@ -475,11 +484,4 @@ public function testDuplicateRemoveEmbargoExpiry() { $this->assertEquals($dupe->PublishJobID, 0, 'Publish job ID unset'); $this->assertEquals($dupe->UnPublishJobID, 0, 'Unpublish job ID unset'); } - - public function logOut() - { - if($member = Member::currentUser()) $member->logOut(); - } - - } diff --git a/tests/WorkflowEngineTest.php b/tests/WorkflowEngineTest.php index 70e9be71..db41cc57 100644 --- a/tests/WorkflowEngineTest.php +++ b/tests/WorkflowEngineTest.php @@ -1,8 +1,19 @@ Title = "Create Workflow Instance"; + $definition->write(); + + $stepOne = new WorkflowAction(); + $stepOne->Title = "Step One"; + $stepOne->WorkflowDefID = $definition->ID; + $stepOne->write(); + + $stepTwo = new WorkflowAction(); + $stepTwo->Title = "Step Two"; + $stepTwo->WorkflowDefID = $definition->ID; + $stepTwo->write(); + + $transitionOne = new WorkflowTransition(); + $transitionOne->Title = 'Step One T1'; + $transitionOne->ActionID = $stepOne->ID; + $transitionOne->NextActionID = $stepTwo->ID; + $transitionOne->write(); + + $instance = new WorkflowInstance(); + $instance->write(); + + $instance->beginWorkflow($definition); + + $actions = $definition->Actions(); + $this->assertEquals(2, $actions->Count()); + + $transitions = $actions->find('Title', 'Step One')->Transitions(); + $this->assertEquals(1, $transitions->Count()); + } + + public function testExecuteImmediateWorkflow() + { + $def = $this->createDefinition(); + + $actions = $def->Actions(); + $firstAction = $def->getInitialAction(); + $this->assertEquals('Step One', $firstAction->Title); + + $instance = new WorkflowInstance(); + $instance->beginWorkflow($def); + $this->assertTrue($instance->CurrentActionID > 0); + + $instance->execute(); + + // the instance should be complete, and have two finished workflow action + // instances. + $actions = $instance->Actions(); + $this->assertEquals(2, $actions->Count()); + + foreach ($actions as $action) { + $this->assertTrue((bool) $action->Finished); + } + } + + /** + * Ensure WorkflowInstance returns expected values for a Published target object. + */ + public function testInstanceGetTargetPublished() + { + $def = $this->createDefinition(); + $target = $this->objFromFixture(SiteTree::class, 'published-object'); + $target->publishRecursive(); + + $instance = $this->objFromFixture(WorkflowInstance::class, 'target-is-published'); + $instance->beginWorkflow($def); + $instance->execute(); + + $this->assertTrue($target->isPublished()); + $this->assertEquals($target->ID, $instance->getTarget()->ID); + $this->assertEquals($target->Title, $instance->getTarget()->Title); + } - public static $fixture_file = 'advancedworkflow/tests/workflowinstancetargets.yml'; + /** + * Ensure WorkflowInstance returns expected values for a Draft target object. + */ + public function testInstanceGetTargetDraft() + { + $def = $this->createDefinition(); + $target = $this->objFromFixture(SiteTree::class, 'draft-object'); + + $instance = $this->objFromFixture(WorkflowInstance::class, 'target-is-draft'); + $instance->beginWorkflow($def); + $instance->execute(); + + $this->assertFalse($target->isPublished()); + $this->assertEquals($target->ID, $instance->getTarget()->ID); + $this->assertEquals($target->Title, $instance->getTarget()->Title); + } - public function testCreateWorkflowInstance() { + public function testPublishAction() + { + $this->logInWithPermission(); - $definition = new WorkflowDefinition(); - $definition->Title = "Create Workflow Instance"; - $definition->write(); + $action = new PublishItemWorkflowAction; + $instance = new WorkflowInstance(); - $stepOne = new WorkflowAction(); - $stepOne->Title = "Step One"; - $stepOne->WorkflowDefID = $definition->ID; - $stepOne->write(); + $page = new SiteTree(); + $page->Title = 'stuff'; + $page->write(); - $stepTwo = new WorkflowAction(); - $stepTwo->Title = "Step Two"; - $stepTwo->WorkflowDefID = $definition->ID; - $stepTwo->write(); + $instance->TargetClass = SiteTree::class; + $instance->TargetID = $page->ID; - $transitionOne = new WorkflowTransition(); - $transitionOne->Title = 'Step One T1'; - $transitionOne->ActionID = $stepOne->ID; - $transitionOne->NextActionID = $stepTwo->ID; - $transitionOne->write(); + $this->assertFalse($page->isPublished()); - $instance = new WorkflowInstance(); - $instance->write(); + $action->execute($instance); - $instance->beginWorkflow($definition); + $page = DataObject::get_by_id(SiteTree::class, $page->ID); + $this->assertTrue($page->isPublished()); + } - $actions = $definition->Actions(); - $this->assertEquals(2, $actions->Count()); + public function testCreateDefinitionWithEmptyTitle() + { + $definition = new WorkflowDefinition(); + $definition->Title = ""; + $definition->write(); + $this->assertContains( + 'My Workflow', + $definition->Title, + 'Workflow created without title is assigned a default title.' + ); + } - $transitions = $actions->find('Title', 'Step One')->Transitions(); - $this->assertEquals(1, $transitions->Count()); - } + protected function createDefinition() + { + $definition = new WorkflowDefinition(); + $definition->Title = "Dummy Workflow Definition"; + $definition->write(); + + $stepOne = new WorkflowAction(); + $stepOne->Title = "Step One"; + $stepOne->WorkflowDefID = $definition->ID; + $stepOne->write(); + + $stepTwo = new WorkflowAction(); + $stepTwo->Title = "Step Two"; + $stepTwo->WorkflowDefID = $definition->ID; + $stepTwo->write(); + + $transitionOne = new WorkflowTransition(); + $transitionOne->Title = 'Step One T1'; + $transitionOne->ActionID = $stepOne->ID; + $transitionOne->NextActionID = $stepTwo->ID; + $transitionOne->write(); + + return $definition; + } - public function testExecuteImmediateWorkflow() { - $def = $this->createDefinition(); - - $actions = $def->Actions(); - $firstAction = $def->getInitialAction(); - $this->assertEquals('Step One', $firstAction->Title); - - $instance = new WorkflowInstance(); - $instance->beginWorkflow($def); - $this->assertTrue($instance->CurrentActionID > 0); - - $instance->execute(); + public function testCreateFromTemplate() + { + $structure = array( + 'First step' => array( + 'type' => AssignUsersToWorkflowAction::class, + 'transitions' => array( + 'second' => 'Second step' + ) + ), + 'Second step' => array( + 'type' => NotifyUsersWorkflowAction::class, + 'transitions' => array( + 'Approve' => 'Third step' + ) + ), + ); - // the instance should be complete, and have two finished workflow action - // instances. - $actions = $instance->Actions(); - $this->assertEquals(2, $actions->Count()); - - foreach($actions as $action) { - $this->assertTrue((bool) $action->Finished); - } - } - - /** - * Ensure WorkflowInstance returns expected values for a Published target object. - */ - public function testInstanceGetTargetPublished() { - $def = $this->createDefinition(); - $target = $this->objFromFixture('SiteTree', 'published-object'); - $target->doPublish(); - - $instance = $this->objFromFixture('WorkflowInstance', 'target-is-published'); - $instance->beginWorkflow($def); - $instance->execute(); - - $this->assertTrue($target->isPublished()); - $this->assertEquals($target->ID, $instance->getTarget()->ID); - $this->assertEquals($target->Title, $instance->getTarget()->Title); - } - - /** - * Ensure WorkflowInstance returns expected values for a Draft target object. - */ - public function testInstanceGetTargetDraft() { - $def = $this->createDefinition(); - $target = $this->objFromFixture('SiteTree', 'draft-object'); + $template = new WorkflowTemplate('Test'); - $instance = $this->objFromFixture('WorkflowInstance', 'target-is-draft'); - $instance->beginWorkflow($def); - $instance->execute(); + $template->setStructure($structure); - $this->assertFalse($target->isPublished()); - $this->assertEquals($target->ID, $instance->getTarget()->ID); - $this->assertEquals($target->Title, $instance->getTarget()->Title); - } + $actions = $template->createRelations(); - public function testPublishAction() { - $this->logInWithPermission(); + $this->assertEquals(2, count($actions)); + $this->assertTrue(isset($actions['First step'])); + $this->assertTrue(isset($actions['Second step'])); - $action = new PublishItemWorkflowAction; - $instance = new WorkflowInstance(); + $this->assertTrue($actions['First step']->exists()); - $page = new SiteTree(); - $page->Title = 'stuff'; - $page->write(); - - $instance->TargetClass = 'SiteTree'; - $instance->TargetID = $page->ID; - - $this->assertFalse($page->isPublished()); - - $action->execute($instance); - - $page = DataObject::get_by_id('SiteTree', $page->ID); - $this->assertTrue($page->isPublished()); - - } - - public function testCreateDefinitionWithEmptyTitle() { - $definition = new WorkflowDefinition(); - $definition->Title = ""; - $definition->write(); - $this->assertContains( - 'My Workflow', - $definition->Title, - 'Workflow created without title is assigned a default title.' - ); - } - - protected function createDefinition() { - $definition = new WorkflowDefinition(); - $definition->Title = "Dummy Workflow Definition"; - $definition->write(); - - $stepOne = new WorkflowAction(); - $stepOne->Title = "Step One"; - $stepOne->WorkflowDefID = $definition->ID; - $stepOne->write(); - - $stepTwo = new WorkflowAction(); - $stepTwo->Title = "Step Two"; - $stepTwo->WorkflowDefID = $definition->ID; - $stepTwo->write(); - - $transitionOne = new WorkflowTransition(); - $transitionOne->Title = 'Step One T1'; - $transitionOne->ActionID = $stepOne->ID; - $transitionOne->NextActionID = $stepTwo->ID; - $transitionOne->write(); - - return $definition; - } - - - public function testCreateFromTemplate() { - $structure = array( - 'First step' => array( - 'type' => 'AssignUsersToWorkflowAction', - 'transitions' => array( - 'second' => 'Second step' - ) - ), - 'Second step' => array( - 'type' => 'NotifyUsersWorkflowAction', - 'transitions' => array( - 'Approve' => 'Third step' - ) - ), - ); - - $template = new WorkflowTemplate('Test'); - - $template->setStructure($structure); - - $actions = $template->createRelations(); - - $this->assertEquals(2, count($actions)); - $this->assertTrue(isset($actions['First step'])); - $this->assertTrue(isset($actions['Second step'])); - - $this->assertTrue($actions['First step']->exists()); - - $transitions = $actions['First step']->Transitions(); - - $this->assertTrue($transitions->count() == 1); - - - } - - /** - * Tests whether if user(s) are able to delete a workflow, dependent on permissions. - */ - public function testCanDeleteWorkflow() { - // Create a definition - $def = $this->createDefinition(); - - // Test a user with lame permissions - $memberID = $this->logInWithPermission('SITETREE_VIEW_ALL'); - $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID); - $this->assertFalse($def->canCreate($member)); - - // Test a user with good permissions - $memberID = $this->logInWithPermission('CREATE_WORKFLOW'); - $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID); - $this->assertTrue($def->canCreate($member)); - } - - /** - * For a context around this test, see: https://github.com/symbiote/advancedworkflow/issues/141 - * - * 1). Create a workflow definition - * 2). Step the content into that workflow - * 3). Delete the workflow - * 4). Check that the content: - * i). Has no remaining related actions - * ii). Can be re-assigned a new Workflow - * 5). Check that the object under workflow, maintains its status (Draft, Published etc) - */ - public function testDeleteWorkflowTargetStillWorks() { - // 1). Create a workflow definition - $def = $this->createDefinition(); - $page = SiteTree::create(); - $page->Title = 'dummy test'; - $page->WorkflowDefinitionID = $def->ID; // Normally done via CMS - Versioned::set_stage(Versioned::DRAFT); - $page->write(); - - // Check $page is in draft, pre-deletion - $status = ($page->getIsAddedToStage() && !$page->getExistsOnLive()); - $this->assertTrue($status); - - // 2). Step the content into that workflow - $instance = new WorkflowInstance(); - $instance->beginWorkflow($def, $page); - $instance->execute(); - - // Check the content is assigned - $testPage = DataObject::get_by_id('SiteTree', $page->ID); - $this->assertEquals($instance->TargetID, $testPage->ID); - - // 3). Delete the workflow - $def->delete(); - - // Check $testPage is _still_ in draft, post-deletion - $status = ($testPage->getIsAddedToStage() && !$testPage->getExistsOnLive()); - $this->assertTrue($status); - - /* + $transitions = $actions['First step']->Transitions(); + + $this->assertTrue($transitions->count() == 1); + } + + /** + * Tests whether if user(s) are able to delete a workflow, dependent on permissions. + */ + public function testCanDeleteWorkflow() + { + // Create a definition + $def = $this->createDefinition(); + + // Test a user with lame permissions + $memberID = $this->logInWithPermission('SITETREE_VIEW_ALL'); + $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID); + $this->assertFalse($def->canCreate($member)); + + // Test a user with good permissions + $memberID = $this->logInWithPermission('CREATE_WORKFLOW'); + $member = DataObject::get_by_id('SilverStripe\\Security\\Member', $memberID); + $this->assertTrue($def->canCreate($member)); + } + + /** + * For a context around this test, see: https://github.com/symbiote/advancedworkflow/issues/141 + * + * 1). Create a workflow definition + * 2). Step the content into that workflow + * 3). Delete the workflow + * 4). Check that the content: + * i). Has no remaining related actions + * ii). Can be re-assigned a new Workflow + * 5). Check that the object under workflow, maintains its status (Draft, Published etc) + */ + public function testDeleteWorkflowTargetStillWorks() + { + // 1). Create a workflow definition + $def = $this->createDefinition(); + $page = SiteTree::create(); + $page->Title = 'dummy test'; + $page->WorkflowDefinitionID = $def->ID; // Normally done via CMS + Versioned::set_stage(Versioned::DRAFT); + $page->write(); + + // Check $page is in draft, pre-deletion + $status = ($page->isOnDraftOnly() && !$page->isPublished()); + $this->assertTrue($status); + + // 2). Step the content into that workflow + $instance = new WorkflowInstance(); + $instance->beginWorkflow($def, $page); + $instance->execute(); + + // Check the content is assigned + $testPage = DataObject::get_by_id(SiteTree::class, $page->ID); + $this->assertEquals($instance->TargetID, $testPage->ID); + + // 3). Delete the workflow + $def->delete(); + + // Check $testPage is _still_ in draft, post-deletion + $status = ($testPage->isOnDraftOnly() && !$testPage->isPublished()); + $this->assertTrue($status); + + /* * 4). i). Check that the content: Has no remaining related actions * Note: WorkflowApplicable::WorkflowDefinitionID does _not_ get updated until assigned a new workflow * so we can use it to check that all related actions are gone */ - $defID = $testPage->WorkflowDefinitionID; - $this->assertEquals(0, DataObject::get('WorkflowAction')->filter('WorkflowDefID', $defID)->count()); + $defID = $testPage->WorkflowDefinitionID; + $this->assertEquals(0, DataObject::get(WorkflowAction::class)->filter('WorkflowDefID', $defID)->count()); - /* + /* * 4). ii). Check that the content: Can be re-assigned a new Workflow Definition */ - $newDef = $this->createDefinition(); - $testPage->WorkflowDefinitionID = $newDef->ID; // Normally done via CMS - $instance = new WorkflowInstance(); - $instance->beginWorkflow($newDef, $testPage); - $instance->execute(); - - // Check the content is assigned to the new Workflow Definition correctly - $this->assertEquals($newDef->ID, $testPage->WorkflowDefinitionID); - $this->assertEquals( - $newDef->Actions()->count(), - DataObject::get('WorkflowAction')->filter('WorkflowDefID', $newDef->ID)->count() - ); - - // 5). Check that the object under workflow, maintains its status - $newDef2 = $this->createDefinition(); - - // Login so SiteTree::canPublish() returns true - $testPage->WorkflowDefinitionID = $newDef2->ID; // Normally done via CMS - $this->logInWithPermission(); - $testPage->doPublish(); - - // Check $testPage is published, pre-deletion (getStatusFlags() returns empty array) - $this->assertTrue($testPage->getExistsOnLive()); - - $instance = new WorkflowInstance(); - $instance->beginWorkflow($newDef2, $testPage); - $instance->execute(); - - // Now delete the related WorkflowDefinition and ensure status is the same - // (i.e. so it's not 'modified' for example) - $newDef2->delete(); - - // Check $testPage is _still_ published, post-deletion (getStatusFlags() returns empty array) - $this->assertTrue($testPage->getExistsOnLive()); - } + $newDef = $this->createDefinition(); + $testPage->WorkflowDefinitionID = $newDef->ID; // Normally done via CMS + $instance = new WorkflowInstance(); + $instance->beginWorkflow($newDef, $testPage); + $instance->execute(); + + // Check the content is assigned to the new Workflow Definition correctly + $this->assertEquals($newDef->ID, $testPage->WorkflowDefinitionID); + $this->assertEquals( + $newDef->Actions()->count(), + DataObject::get(WorkflowAction::class)->filter('WorkflowDefID', $newDef->ID)->count() + ); + + // 5). Check that the object under workflow, maintains its status + $newDef2 = $this->createDefinition(); + + // Login so SiteTree::canPublish() returns true + $testPage->WorkflowDefinitionID = $newDef2->ID; // Normally done via CMS + $this->logInWithPermission(); + $testPage->publishRecursive(); + + // Check $testPage is published, pre-deletion (getStatusFlags() returns empty array) + $this->assertTrue($testPage->isPublished()); + + $instance = new WorkflowInstance(); + $instance->beginWorkflow($newDef2, $testPage); + $instance->execute(); + + // Now delete the related WorkflowDefinition and ensure status is the same + // (i.e. so it's not 'modified' for example) + $newDef2->delete(); + + // Check $testPage is _still_ published, post-deletion (getStatusFlags() returns empty array) + $this->assertTrue($testPage->isPublished()); + } /** * Test the diff showing only fields that have changes made to it in a data object. */ public function testInstanceDiff() { - $instance = $this->objFromFixture('WorkflowInstance', 'target-is-published'); + $instance = $this->objFromFixture(WorkflowInstance::class, 'target-is-published'); $target = $instance->getTarget(); - $target->doPublish(); + $target->publishRecursive(); $target->Title = 'New title for target'; $target->write(); @@ -320,5 +336,4 @@ public function testInstanceDiff() $this->assertContains('Title', $diff); $this->assertNotContains('Content', $diff); } - } diff --git a/tests/WorkflowImportExportTest.php b/tests/WorkflowImportExportTest.php index 88969014..b8f42b51 100644 --- a/tests/WorkflowImportExportTest.php +++ b/tests/WorkflowImportExportTest.php @@ -1,7 +1,18 @@ Title = "Dummy Workflow Definition"; - $definition->write(); - - $stepOne = new WorkflowAction(); - $stepOne->Title = "Step One"; - $stepOne->WorkflowDefID = $definition->ID; - $stepOne->write(); - - $stepTwo = new WorkflowAction(); - $stepTwo->Title = "Step Two"; - $stepTwo->WorkflowDefID = $definition->ID; - $stepTwo->write(); - - $transitionOne = new WorkflowTransition(); - $transitionOne->Title = 'Step One T1'; - $transitionOne->ActionID = $stepOne->ID; - $transitionOne->NextActionID = $stepTwo->ID; - $transitionOne->write(); - - return $definition; - } - - /** - * Create a WorkflowDefinition with some actions. Ensure an expected length of formatted template. - */ - public function testFormatWithActions() { - $definition = $this->createDefinition(); - $exporter = Injector::inst()->createWithArgs('WorkflowDefinitionExporter', array($definition->ID)); - $member = new Member(); - $member->FirstName = 'joe'; - $member->Surname = 'bloggs'; - $exporter->setMember($member); - $templateData = new ArrayData(array( - 'ExportMetaData' => $exporter->ExportMetaData(), - 'ExportActions' => $exporter->getDefinition()->Actions() - )); - - $formatted = $exporter->format($templateData); - $numActions = count(preg_split("#\R#", $formatted)); - - $this->assertNotEmpty($formatted); - // Seems arbitrary, but if no actions, then the resulting YAML file is exactly 18 lines long - $this->assertGreaterThan(18, $numActions); - } - - /** - * Create a WorkflowDefinition with NO actions. Ensure an expected length of formatted template. - */ - public function testFormatWithoutActions() { - $definition = $this->createDefinition(); - $exporter = Injector::inst()->createWithArgs('WorkflowDefinitionExporter', array($definition->ID)); - $member = new Member(); - $member->FirstName = 'joe'; - $member->Surname = 'bloggs'; - $exporter->setMember($member); - $templateData = new ArrayData(array()); - - $formatted = $exporter->format($templateData); - $numActions = count(preg_split("#\R#", $formatted)); - - // Seems arbitrary, but if no actions, then the resulting YAML file is exactly 18 lines long - $this->assertEquals(18, $numActions); - - // Ensure outputted YAML has no blank lines, where SS's control structures would normally be - $numBlanks = preg_match("#^\s*$#m", $formatted); - $this->assertEquals(0, $numBlanks); - } - - /** - * Tests a badly formatted YAML import for parsing (no headers) - * Note: The available test-cases we can expect to get out of sfYamlParser is limited.. - */ - public function testParseBadYAMLNoHeaderImport() { - $importer = new WorkflowDefinitionImporter(); - $this->setExpectedException('Exception', 'Invalid YAML format.'); - $source = <<<'EOD' -Injector: +class WorkflowImportExportTest extends SapphireTest +{ + protected static $fixture_file = 'workflowtemplateimport.yml'; + + /** + * Utility method, used in tests + * @return WorkflowDefinition + */ + protected function createDefinition() + { + $definition = new WorkflowDefinition(); + $definition->Title = "Dummy Workflow Definition"; + $definition->write(); + + $stepOne = new WorkflowAction(); + $stepOne->Title = "Step One"; + $stepOne->WorkflowDefID = $definition->ID; + $stepOne->write(); + + $stepTwo = new WorkflowAction(); + $stepTwo->Title = "Step Two"; + $stepTwo->WorkflowDefID = $definition->ID; + $stepTwo->write(); + + $transitionOne = new WorkflowTransition(); + $transitionOne->Title = 'Step One T1'; + $transitionOne->ActionID = $stepOne->ID; + $transitionOne->NextActionID = $stepTwo->ID; + $transitionOne->write(); + + return $definition; + } + + /** + * Create a WorkflowDefinition with some actions. Ensure an expected length of formatted template. + */ + public function testFormatWithActions() + { + $definition = $this->createDefinition(); + $exporter = Injector::inst()->createWithArgs(WorkflowDefinitionExporter::class, array($definition->ID)); + $member = new Member(); + $member->FirstName = 'joe'; + $member->Surname = 'bloggs'; + $exporter->setMember($member); + $templateData = new ArrayData(array( + 'ExportMetaData' => $exporter->ExportMetaData(), + 'ExportActions' => $exporter->getDefinition()->Actions() + )); + + $formatted = $exporter->format($templateData); + $numActions = count(preg_split("#\R#", $formatted)); + + $this->assertNotEmpty($formatted); + // Seems arbitrary, but if no actions, then the resulting YAML file is exactly 18 lines long + $this->assertGreaterThan(18, $numActions); + } + + /** + * Create a WorkflowDefinition with NO actions. Ensure an expected length of formatted template. + */ + public function testFormatWithoutActions() + { + $definition = $this->createDefinition(); + $exporter = Injector::inst()->createWithArgs(WorkflowDefinitionExporter::class, array($definition->ID)); + $member = new Member(); + $member->FirstName = 'joe'; + $member->Surname = 'bloggs'; + $exporter->setMember($member); + $templateData = new ArrayData(array()); + + $formatted = $exporter->format($templateData); + $numActions = count(preg_split("#\R#", $formatted)); + + // Seems arbitrary, but if no actions, then the resulting YAML file is exactly 18 lines long + $this->assertEquals(18, $numActions); + + // Ensure outputted YAML has no blank lines, where SS's control structures would normally be + $numBlanks = preg_match("#^\s*$#m", $formatted); + $this->assertEquals(0, $numBlanks); + } + + /** + * Tests a badly formatted YAML import for parsing (no headers) + */ + public function testParseBadYAMLNoHeaderImport() + { + $importer = new WorkflowDefinitionImporter(); + $this->setExpectedException('Exception', 'Invalid YAML format.'); + $source = <<<'EOD' +SilverStripe\Core\Injector\Injector\Injector: ExportedWorkflow: - class: WorkflowTemplate + class: Symbiote\AdvancedWorkflow\Templates\WorkflowTemplate constructor: - 'My Workflow 4 20/02/2014 03-12-55' - - 'Exported from localhost on 20/02/2014 03:12:55 by joe bloggs using SilverStripe versions Framework 3.1.2, CMS 3.1.2' + - 'Exported from localhost on 20/02/2014 03:12:55 by joe bloggs using SilverStripe versions Framework 4.0.0-beta3' - 0.2 - 0 - 3 properties: structure: 'Step One': - type: WorkflowAction + type: Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction transitions: - Step One T1: 'Step Two' 'Step Two': - type: WorkflowAction - WorkflowService: + type: Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction + Symbiote\AdvancedWorkflow\Services\WorkflowService: properties: templates: - %$ExportedWorkflow EOD; - - $importer->parseYAMLImport($source); - } - - /** - * Tests a badly formatted YAML import for parsing (missing YML colon) - * Note: The available test-cases we can expect to get out of sfYamlParser is limited.. - */ - public function testParseBadYAMLMalformedImport() { - $importer = new WorkflowDefinitionImporter(); - $this->setExpectedException('SilverStripe\\ORM\\ValidationException', 'Invalid YAML format. Unable to parse.'); - $source = <<<'EOD' + + $importer->parseYAMLImport($source); + } + + /** + * Tests a badly formatted YAML import for parsing (missing YML colon) + */ + public function testParseBadYAMLMalformedImport() + { + $importer = new WorkflowDefinitionImporter(); + $this->setExpectedException('SilverStripe\\ORM\\ValidationException', 'Invalid YAML format. Unable to parse.'); + $source = <<<'EOD' --- Name: exportedworkflow --- -Injector: +# Missing colon on line below +SilverStripe\Core\Injector\Injector ExportedWorkflow: - class: WorkflowTemplate + class: Symbiote\AdvancedWorkflow\Templates\WorkflowTemplate constructor: - 'My Workflow 4 20/02/2014 03-12-55' - - 'Exported from localhost on 20/02/2014 03-12-55 by joe bloggs using SilverStripe versions Framework 3.1.2, CMS 3.1.2' + - 'Exported from localhost on 20/02/2014 03-12-55 by joe bloggs using SilverStripe versions Framework 4.0.0-beta3' - 0.2 - 0 - 3 properties: structure: 'Step One' - type: WorkflowAction + type: Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction transitions: - Step One T1: 'Step Two' 'Step Two': - type: WorkflowAction - WorkflowService: + type: Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction + Symbiote\AdvancedWorkflow\Services\WorkflowService: properties: templates: - %$ExportedWorkflow EOD; - - $importer->parseYAMLImport($source); - } - - /** - * Tests a well-formatted YAML import for parsing - * Note: The available test-cases we can expect to get out of sfYamlParser is limited.. - */ - public function testParseGoodYAMLImport() { - $importer = new WorkflowDefinitionImporter(); - $source = <<<'EOD' + + $importer->parseYAMLImport($source); + } + + /** + * Tests a well-formatted YAML import for parsing + */ + public function testParseGoodYAMLImport() + { + $importer = new WorkflowDefinitionImporter(); + $source = <<<'EOD' --- Name: exportedworkflow --- -Injector: +SilverStripe\Core\Injector\Injector: ExportedWorkflow: - class: WorkflowTemplate + class: Symbiote\AdvancedWorkflow\Templates\WorkflowTemplate constructor: - 'My Workflow 4 20/02/2014 03-12-55' - - 'Exported from localhost on 20/02/2014 03-12-55 by joe bloggs using SilverStripe versions Framework 3.1.2, CMS 3.1.2' + - 'Exported from localhost on 20/02/2014 03-12-55 by joe bloggs using SilverStripe versions Framework 4.0.0-beta3' - 0.2 - 0 - 3 properties: structure: 'Step One': - type: WorkflowAction + type: Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction transitions: - Step One T1: 'Step Two' 'Step Two': - type: WorkflowAction - WorkflowService: + type: Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction + Symbiote\AdvancedWorkflow\Services\WorkflowService: properties: templates: - %$ExportedWorkflow EOD; - - $this->assertNotEmpty($importer->parseYAMLImport($source)); - } - - /** - * Given no ImportedWorkflowTemplate fixture/input data, tests an empty array is returned - * by WorkflowDefinitionImporter#getImportedWorkflows() - */ - public function testGetImportedWorkflowsNone() { - $this->clearFixtures(); - $importer = new WorkflowDefinitionImporter(); - $imports = $importer->getImportedWorkflows(); - $this->assertEmpty($imports); - } - - /** - * Given a single ImportedWorkflowTemplate fixture/input data, tests an non-empty array is returned - * by WorkflowDefinitionImporter#getImportedWorkflows() - */ - public function testGetImportedWorkflowsOne() { - $name = 'My Workflow 21/02/2014 09-01-29'; - // Pretend a ImportedWorkflowTemplate object has been created by WorkflowBulkLoader - $this->objFromFixture('ImportedWorkflowTemplate', 'Import01'); - - $importer = singleton('WorkflowDefinitionImporter'); - $import = $importer->getImportedWorkflows($name); - - $this->assertNotEmpty($import); - $this->assertInstanceOf('WorkflowTemplate', $import); - $this->assertEquals(1, count($import)); - $this->assertEquals($name, $import->getName()); - } - - /** - * Given many ImportedWorkflowTemplate fixture/input data, tests an non-empty array is returned - * by WorkflowDefinitionImporter#getImportedWorkflows() - */ - public function testGetImportedWorkflowsMany() { - // Pretend some ImportedWorkflowTemplate objects have been created by WorkflowBulkLoader - $this->objFromFixture('ImportedWorkflowTemplate', 'Import02'); - $this->objFromFixture('ImportedWorkflowTemplate', 'Import03'); - - $importer = singleton('WorkflowDefinitionImporter'); - $imports = $importer->getImportedWorkflows(); - - $this->assertNotEmpty($imports); - $this->assertInternalType('array', $imports); - $this->assertGreaterThan(1, count($imports)); - } - -} \ No newline at end of file + + $this->assertNotEmpty($importer->parseYAMLImport($source)); + } + + /** + * Given no ImportedWorkflowTemplate fixture/input data, tests an empty array is returned + * by WorkflowDefinitionImporter#getImportedWorkflows() + */ + public function testGetImportedWorkflowsNone() + { + $this->clearFixtures(); + $importer = new WorkflowDefinitionImporter(); + $imports = $importer->getImportedWorkflows(); + $this->assertEmpty($imports); + } + + /** + * Given a single ImportedWorkflowTemplate fixture/input data, tests an non-empty array is returned + * by WorkflowDefinitionImporter#getImportedWorkflows() + */ + public function testGetImportedWorkflowsOne() + { + $name = 'My Workflow 21/02/2014 09-01-29'; + // Pretend a ImportedWorkflowTemplate object has been created by WorkflowBulkLoader + $this->objFromFixture(ImportedWorkflowTemplate::class, 'Import01'); + + $importer = singleton(WorkflowDefinitionImporter::class); + $import = $importer->getImportedWorkflows($name); + + $this->assertNotEmpty($import); + $this->assertInstanceOf(WorkflowTemplate::class, $import); + $this->assertEquals(1, count($import)); + $this->assertEquals($name, $import->getName()); + } + + /** + * Given many ImportedWorkflowTemplate fixture/input data, tests an non-empty array is returned + * by WorkflowDefinitionImporter#getImportedWorkflows() + */ + public function testGetImportedWorkflowsMany() + { + // Pretend some ImportedWorkflowTemplate objects have been created by WorkflowBulkLoader + $this->objFromFixture(ImportedWorkflowTemplate::class, 'Import02'); + $this->objFromFixture(ImportedWorkflowTemplate::class, 'Import03'); + + $importer = singleton(WorkflowDefinitionImporter::class); + $imports = $importer->getImportedWorkflows(); + + $this->assertNotEmpty($imports); + $this->assertInternalType('array', $imports); + $this->assertGreaterThan(1, count($imports)); + } +} diff --git a/tests/WorkflowInstanceTest.php b/tests/WorkflowInstanceTest.php index 7fb5e6fd..99d1cbb8 100644 --- a/tests/WorkflowInstanceTest.php +++ b/tests/WorkflowInstanceTest.php @@ -1,6 +1,12 @@ currentMember = $this->objFromFixture('SilverStripe\\Security\\Member', 'ApproverMember01'); - } - - /** - * Tests WorkflowInstance#getMostRecentActionForUser() - */ - public function testGetMostRecentActionForUser() { - - // Single, AssignUsersToWorkflowAction in "History" - $history01 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance01'); - $mostRecentActionForUser01 = $history01->getMostRecentActionForUser($this->currentMember); - $this->assertInstanceOf('WorkflowActionInstance', $mostRecentActionForUser01, 'Asserts the correct ClassName is retured #1'); - $this->assertEquals('Assign', $mostRecentActionForUser01->BaseAction()->Title, 'Asserts the correct BaseAction is retured #1'); - - // No AssignUsersToWorkflowAction found with Member's related Group in "History" - $history02 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance02'); - $mostRecentActionForUser02 = $history02->getMostRecentActionForUser($this->currentMember); - $this->assertFalse($mostRecentActionForUser02, 'Asserts false is returned because no WorkflowActionInstance was found'); - - // Multiple AssignUsersToWorkflowAction in "History", only one with Group relations - $history03 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance03'); - $mostRecentActionForUser03 = $history03->getMostRecentActionForUser($this->currentMember); - $this->assertInstanceOf('WorkflowActionInstance', $mostRecentActionForUser03, 'Asserts the correct ClassName is retured #2'); - $this->assertEquals('Assign', $mostRecentActionForUser03->BaseAction()->Title, 'Asserts the correct BaseAction is retured #2'); - - // Multiple AssignUsersToWorkflowAction in "History", both with Group relations - $history04 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance04'); - $mostRecentActionForUser04 = $history04->getMostRecentActionForUser($this->currentMember); - $this->assertInstanceOf('WorkflowActionInstance', $mostRecentActionForUser04, 'Asserts the correct ClassName is retured #3'); - $this->assertEquals('Assigned Again', $mostRecentActionForUser04->BaseAction()->Title, 'Asserts the correct BaseAction is retured #3'); - - // Multiple AssignUsersToWorkflowAction in "History", one with Group relations - $history05 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance05'); - $mostRecentActionForUser05 = $history05->getMostRecentActionForUser($this->currentMember); - $this->assertInstanceOf('WorkflowActionInstance', $mostRecentActionForUser05, 'Asserts the correct ClassName is retured #4'); - $this->assertEquals('Assigned Me', $mostRecentActionForUser05->BaseAction()->Title, 'Asserts the correct BaseAction is retured #4'); - } - - /** - * Tests WorkflowInstance#canView() - */ - public function testCanView() { - // Single, AssignUsersToWorkflowAction in "History" - $history01 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance01'); - $this->assertTrue($history01->canView($this->currentMember)); - - // No AssignUsersToWorkflowAction found with Member's related Group in "History" - $history02 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance02'); - $this->assertFalse($history02->canView($this->currentMember)); - - // Multiple AssignUsersToWorkflowAction in "History", only one with Group relations - $history03 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance03'); - $this->assertTrue($history03->canView($this->currentMember)); - - // Multiple AssignUsersToWorkflowAction in "History", both with Group relations - $history04 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance04'); - $this->assertTrue($history04->canView($this->currentMember)); - - // Multiple AssignUsersToWorkflowAction in "History" - $history05 = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance05'); - $this->assertTrue($history05->canView($this->currentMember)); - } - - public function testValidTransitions() { - $instance = $this->objFromFixture('WorkflowInstance', 'WorkflowInstance06'); - $transition1 = $this->objFromFixture('WorkflowTransition', 'Transition05'); - $transition2 = $this->objFromFixture('WorkflowTransition', 'Transition06'); - $member1 = $this->objFromFixture('SilverStripe\\Security\\Member', 'Transition05Member'); - $member2 = $this->objFromFixture('SilverStripe\\Security\\Member', 'Transition06Member'); - - // Given logged in as admin, check that there are two actions - $this->logInWithPermission('ADMIN'); - $transitions = $instance->validTransitions()->column('ID'); - $this->assertContains($transition1->ID, $transitions); - $this->assertContains($transition2->ID, $transitions); - - // Logged in as a member with permission on one transition, check that only this one is present - $member1->logIn(); - $transitions = $instance->validTransitions()->column('ID'); - $this->assertContains($transition1->ID, $transitions); - $this->assertNotContains($transition2->ID, $transitions); - - // Logged in as a member with permissions via group - $member2->logIn(); - $transitions = $instance->validTransitions()->column('ID'); - $this->assertNotContains($transition1->ID, $transitions); - $this->assertContains($transition2->ID, $transitions); - } - -} \ No newline at end of file +class WorkflowInstanceTest extends SapphireTest +{ + /** + * @var string + */ + protected static $fixture_file = 'useractioninstancehistory.yml'; + + /** + * @var Member + */ + protected $currentMember; + + protected function setUp() + { + parent::setUp(); + $this->currentMember = $this->objFromFixture(Member::class, 'ApproverMember01'); + } + + /** + * Tests WorkflowInstance#getMostRecentActionForUser() + */ + public function testGetMostRecentActionForUser() + { + // Single, AssignUsersToWorkflowAction in "History" + $history01 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance01'); + $mostRecentActionForUser01 = $history01->getMostRecentActionForUser($this->currentMember); + $this->assertInstanceOf(WorkflowActionInstance::class, $mostRecentActionForUser01, 'Asserts the correct ClassName is retured #1'); + $this->assertEquals('Assign', $mostRecentActionForUser01->BaseAction()->Title, 'Asserts the correct BaseAction is retured #1'); + + // No AssignUsersToWorkflowAction found with Member's related Group in "History" + $history02 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance02'); + $mostRecentActionForUser02 = $history02->getMostRecentActionForUser($this->currentMember); + $this->assertFalse($mostRecentActionForUser02, 'Asserts false is returned because no WorkflowActionInstance was found'); + + // Multiple AssignUsersToWorkflowAction in "History", only one with Group relations + $history03 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance03'); + $mostRecentActionForUser03 = $history03->getMostRecentActionForUser($this->currentMember); + $this->assertInstanceOf(WorkflowActionInstance::class, $mostRecentActionForUser03, 'Asserts the correct ClassName is retured #2'); + $this->assertEquals('Assign', $mostRecentActionForUser03->BaseAction()->Title, 'Asserts the correct BaseAction is retured #2'); + + // Multiple AssignUsersToWorkflowAction in "History", both with Group relations + $history04 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance04'); + $mostRecentActionForUser04 = $history04->getMostRecentActionForUser($this->currentMember); + $this->assertInstanceOf(WorkflowActionInstance::class, $mostRecentActionForUser04, 'Asserts the correct ClassName is retured #3'); + $this->assertEquals('Assigned Again', $mostRecentActionForUser04->BaseAction()->Title, 'Asserts the correct BaseAction is retured #3'); + + // Multiple AssignUsersToWorkflowAction in "History", one with Group relations + $history05 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance05'); + $mostRecentActionForUser05 = $history05->getMostRecentActionForUser($this->currentMember); + $this->assertInstanceOf(WorkflowActionInstance::class, $mostRecentActionForUser05, 'Asserts the correct ClassName is retured #4'); + $this->assertEquals('Assigned Me', $mostRecentActionForUser05->BaseAction()->Title, 'Asserts the correct BaseAction is retured #4'); + } + + /** + * Tests WorkflowInstance#canView() + */ + public function testCanView() + { + // Single, AssignUsersToWorkflowAction in "History" + $history01 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance01'); + $this->assertTrue($history01->canView($this->currentMember)); + + // No AssignUsersToWorkflowAction found with Member's related Group in "History" + $history02 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance02'); + $this->assertFalse($history02->canView($this->currentMember)); + + // Multiple AssignUsersToWorkflowAction in "History", only one with Group relations + $history03 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance03'); + $this->assertTrue($history03->canView($this->currentMember)); + + // Multiple AssignUsersToWorkflowAction in "History", both with Group relations + $history04 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance04'); + $this->assertTrue($history04->canView($this->currentMember)); + + // Multiple AssignUsersToWorkflowAction in "History" + $history05 = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance05'); + $this->assertTrue($history05->canView($this->currentMember)); + } + + public function testValidTransitions() + { + $instance = $this->objFromFixture(WorkflowInstance::class, 'WorkflowInstance06'); + $transition1 = $this->objFromFixture(WorkflowTransition::class, 'Transition05'); + $transition2 = $this->objFromFixture(WorkflowTransition::class, 'Transition06'); + $member1 = $this->objFromFixture(Member::class, 'Transition05Member'); + $member2 = $this->objFromFixture(Member::class, 'Transition06Member'); + + // Given logged in as admin, check that there are two actions + $this->logInWithPermission('ADMIN'); + $transitions = $instance->validTransitions()->column('ID'); + $this->assertContains($transition1->ID, $transitions); + $this->assertContains($transition2->ID, $transitions); + + // Logged in as a member with permission on one transition, check that only this one is present + $member1->logIn(); + $transitions = $instance->validTransitions()->column('ID'); + $this->assertContains($transition1->ID, $transitions); + $this->assertNotContains($transition2->ID, $transitions); + + // Logged in as a member with permissions via group + $member2->logIn(); + $transitions = $instance->validTransitions()->column('ID'); + $this->assertNotContains($transition1->ID, $transitions); + $this->assertContains($transition2->ID, $transitions); + } +} diff --git a/tests/WorkflowPermissionsTest.php b/tests/WorkflowPermissionsTest.php index b600c1e6..2e5df8a5 100644 --- a/tests/WorkflowPermissionsTest.php +++ b/tests/WorkflowPermissionsTest.php @@ -1,6 +1,9 @@ logInWithPermission('CMS_ACCESS_AdvancedWorkflowAdmin'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'no-actions'); + $this->assertFalse($workflowdef->canCreate()); + + // Limited perms. No create. + $this->logInWithPermission('VIEW_ACTIVE_WORKFLOWS'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'no-actions'); + $this->assertFalse($workflowdef->canCreate()); + + // Has perms. Can create. + $this->logInWithPermission('CREATE_WORKFLOW'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'no-actions'); + $this->assertTrue($workflowdef->canCreate()); + + // Limited perms. No delete + $this->logInWithPermission('CREATE_WORKFLOW'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'no-actions'); + $this->assertFalse($workflowdef->canDelete()); + + // Has perms. No delete + $this->logInWithPermission('DELETE_WORKFLOW'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'no-actions'); + $this->assertTrue($workflowdef->canDelete()); + } + + /** + * Tests whether members with differing permissions, should be able to create & edit WorkflowActions + */ + public function testWorkflowActionCanPerms() + { + // Very limited perms. No create. + $this->logInWithPermission('CMS_ACCESS_AdvancedWorkflowAdmin'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'with-actions'); + $this->assertFalse($workflowdef->Actions()->first()->canCreate()); + $this->assertFalse($workflowdef->Actions()->first()->canEdit()); + $this->assertFalse($workflowdef->Actions()->first()->canDelete()); + + // Limited perms. No create or delete. + $this->logInWithPermission('VIEW_ACTIVE_WORKFLOWS'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'with-actions'); + $this->assertFalse($workflowdef->Actions()->first()->canCreate()); + $this->assertFalse($workflowdef->Actions()->first()->canCreate()); + $this->assertFalse($workflowdef->Actions()->first()->canDelete()); - /** - * @var string - */ - public static $fixture_file = 'advancedworkflow/tests/workflowpermissions.yml'; - - /** - * Tests whether members with differing permissions, should be able to create & edit WorkflowDefinitions - */ - public function testWorkflowDefinitionCanPerms() { - // Very limited perms. No create. - $this->logInWithPermission('CMS_ACCESS_AdvancedWorkflowAdmin'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'no-actions'); - $this->assertFalse($workflowdef->canCreate()); - - // Limited perms. No create. - $this->logInWithPermission('VIEW_ACTIVE_WORKFLOWS'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'no-actions'); - $this->assertFalse($workflowdef->canCreate()); - - // Has perms. Can create. - $this->logInWithPermission('CREATE_WORKFLOW'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'no-actions'); - $this->assertTrue($workflowdef->canCreate()); + // Has perms. Can create. + $this->logInWithPermission('CREATE_WORKFLOW'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'with-actions'); + $this->assertTrue($workflowdef->Actions()->first()->canCreate()); + $this->assertTrue($workflowdef->Actions()->first()->canEdit()); - // Limited perms. No delete - $this->logInWithPermission('CREATE_WORKFLOW'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'no-actions'); - $this->assertFalse($workflowdef->canDelete()); + // Limited perms. No Delete + $this->assertFalse($workflowdef->Actions()->first()->canDelete()); + } - // Has perms. No delete - $this->logInWithPermission('DELETE_WORKFLOW'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'no-actions'); - $this->assertTrue($workflowdef->canDelete()); - } - - /** - * Tests whether members with differing permissions, should be able to create & edit WorkflowActions - */ - public function testWorkflowActionCanPerms() { - // Very limited perms. No create. - $this->logInWithPermission('CMS_ACCESS_AdvancedWorkflowAdmin'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'with-actions'); - $this->assertFalse($workflowdef->Actions()->first()->canCreate()); - $this->assertFalse($workflowdef->Actions()->first()->canEdit()); - $this->assertFalse($workflowdef->Actions()->first()->canDelete()); - - // Limited perms. No create or delete. - $this->logInWithPermission('VIEW_ACTIVE_WORKFLOWS'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'with-actions'); - $this->assertFalse($workflowdef->Actions()->first()->canCreate()); - $this->assertFalse($workflowdef->Actions()->first()->canCreate()); - $this->assertFalse($workflowdef->Actions()->first()->canDelete()); - - // Has perms. Can create. - $this->logInWithPermission('CREATE_WORKFLOW'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'with-actions'); - $this->assertTrue($workflowdef->Actions()->first()->canCreate()); - $this->assertTrue($workflowdef->Actions()->first()->canEdit()); + /** + * Tests whether members with differing permissions, should be able to create & edit WorkflowActions + */ + public function testWorkflowTransitionPerms() + { + // Very limited perms. No create. + $this->logInWithPermission('CMS_ACCESS_AdvancedWorkflowAdmin'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'with-actions-and-transitions'); + $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canCreate()); + $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canEdit()); + $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canDelete()); - // Limited perms. No Delete - $this->assertFalse($workflowdef->Actions()->first()->canDelete()); - } - - /** - * Tests whether members with differing permissions, should be able to create & edit WorkflowActions - */ - public function testWorkflowTransitionPerms() { - // Very limited perms. No create. - $this->logInWithPermission('CMS_ACCESS_AdvancedWorkflowAdmin'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'with-actions-and-transitions'); - $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canCreate()); - $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canEdit()); - $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canDelete()); - - // Limited perms. No create. - $this->logInWithPermission('VIEW_ACTIVE_WORKFLOWS'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'with-actions-and-transitions'); - $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canCreate()); - $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canEdit()); - $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canDelete()); - - // Has perms. Can create. - $this->logInWithPermission('CREATE_WORKFLOW'); - $workflowdef = $this->objFromFixture('WorkflowDefinition', 'with-actions-and-transitions'); - $this->assertTrue($workflowdef->Actions()->first()->Transitions()->first()->canCreate()); - $this->assertTrue($workflowdef->Actions()->first()->Transitions()->first()->canEdit()); - $this->assertTrue($workflowdef->Actions()->first()->Transitions()->first()->canDelete()); - } + // Limited perms. No create. + $this->logInWithPermission('VIEW_ACTIVE_WORKFLOWS'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'with-actions-and-transitions'); + $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canCreate()); + $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canEdit()); + $this->assertFalse($workflowdef->Actions()->first()->Transitions()->first()->canDelete()); -} \ No newline at end of file + // Has perms. Can create. + $this->logInWithPermission('CREATE_WORKFLOW'); + $workflowdef = $this->objFromFixture(WorkflowDefinition::class, 'with-actions-and-transitions'); + $this->assertTrue($workflowdef->Actions()->first()->Transitions()->first()->canCreate()); + $this->assertTrue($workflowdef->Actions()->first()->Transitions()->first()->canEdit()); + $this->assertTrue($workflowdef->Actions()->first()->Transitions()->first()->canDelete()); + } +} diff --git a/tests/useractioninstancehistory.yml b/tests/useractioninstancehistory.yml index 30ffbbf5..11fb7e84 100644 --- a/tests/useractioninstancehistory.yml +++ b/tests/useractioninstancehistory.yml @@ -25,7 +25,7 @@ SilverStripe\Security\Group: Title: CanExecuteTransition05 Members: =>SilverStripe\Security\Member.Transition05Member -WorkflowTransition: +Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition: Transition01: Title: notify Transition02: @@ -41,132 +41,155 @@ WorkflowTransition: Title: 'Transition with User' Users: =>SilverStripe\Security\Member.Transition06Member -AssignUsersToWorkflowAction: +Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction: BaseAction01: Title: Assign - Transitions: =>WorkflowTransition.Transition01 + Transitions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition01 Groups: =>SilverStripe\Security\Group.BaseAction01Group BaseAction011: Title: 'Another Assignment' - Transitions: =>WorkflowTransition.Transition01 + Transitions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition01 BaseAction012: Title: 'Assigned Again' - Transitions: =>WorkflowTransition.Transition01 + Transitions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition01 Groups: =>SilverStripe\Security\Group.BaseAction01Group BaseAction013: Title: 'Assigned Me' - Transitions: =>WorkflowTransition.Transition01 + Transitions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition01 Groups: =>SilverStripe\Security\Group.BaseAction01Group BaseAction014: Title: 'Not Re-Assigned Me' - Transitions: =>WorkflowTransition.Transition01 -NotifyUsersWorkflowAction: + Transitions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition01 +Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction: BaseAction02: Title: 'Notify users' - Transitions: =>WorkflowTransition.Transition02 + Transitions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition02 BaseAction021: Title: 'Notify users' - Transitions: =>WorkflowTransition.Transition02 -SimpleApprovalWorkflowAction: + Transitions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition02 +Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction: BaseAction03: Title: Approval - Transitions: =>WorkflowTransition.Transition03,=>WorkflowTransition.Transition04 + Transitions: + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition03 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition04 BaseAction031: Title: Approval - Transitions: =>WorkflowTransition.Transition03,=>WorkflowTransition.Transition04 + Transitions: + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition03 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition04 BaseAction032: Title: Approval - Transitions: =>WorkflowTransition.Transition05,=>WorkflowTransition.Transition06 + Transitions: + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition05 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.Transition06 -WorkflowActionInstance: +Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance: ActionInstance01: Created: '2014-02-17 16:15:00' Finished: 1 - BaseAction: =>AssignUsersToWorkflowAction.BaseAction01 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction.BaseAction01 ActionInstance02: Created: '2014-02-17 16:15:01' Finished: 1 - BaseAction: =>NotifyUsersWorkflowAction.BaseAction02 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction.BaseAction02 ActionInstance03: Created: '2014-02-17 16:15:02' Finished: 1 - BaseAction: =>SimpleApprovalWorkflowAction.BaseAction03 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction.BaseAction03 ActionInstance04: Created: '2014-02-17 16:14:59' Finished: 1 - BaseAction: =>AssignUsersToWorkflowAction.BaseAction011 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction.BaseAction011 ActionInstance05: Created: '2014-02-17 16:15:00' Finished: 1 - BaseAction: =>AssignUsersToWorkflowAction.BaseAction01 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction.BaseAction01 ActionInstance06: Created: '2014-02-17 16:15:01' Finished: 1 - BaseAction: =>NotifyUsersWorkflowAction.BaseAction02 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction.BaseAction02 ActionInstance07: Created: '2014-02-17 16:15:02' Finished: 1 - BaseAction: =>SimpleApprovalWorkflowAction.BaseAction03 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction.BaseAction03 ActionInstance08: Created: '2014-02-17 16:15:03' Finished: 1 - BaseAction: =>AssignUsersToWorkflowAction.BaseAction011 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction.BaseAction011 ActionInstance09: Created: '2014-02-17 16:15:00' Finished: 1 - BaseAction: =>AssignUsersToWorkflowAction.BaseAction01 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction.BaseAction01 ActionInstance10: Created: '2014-02-17 16:15:01' Finished: 1 - BaseAction: =>NotifyUsersWorkflowAction.BaseAction02 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction.BaseAction02 ActionInstance11: Created: '2014-02-17 16:15:02' Finished: 1 - BaseAction: =>AssignUsersToWorkflowAction.BaseAction012 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction.BaseAction012 ActionInstance12: Created: '2014-02-17 16:15:00' Finished: 1 - BaseAction: =>AssignUsersToWorkflowAction.BaseAction013 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction.BaseAction013 ActionInstance13: Created: '2014-02-17 16:15:01' Finished: 1 - BaseAction: =>NotifyUsersWorkflowAction.BaseAction021 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction.BaseAction021 ActionInstance14: Created: '2014-02-17 16:15:02' Finished: 1 - BaseAction: =>SimpleApprovalWorkflowAction.BaseAction031 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction.BaseAction031 ActionInstance15: Created: '2014-02-17 16:15:03' Finished: 1 - BaseAction: =>AssignUsersToWorkflowAction.BaseAction014 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction.BaseAction014 ActionInstance16: Created: '2014-02-17 16:15:00' Finished: 1 - BaseAction: =>SimpleApprovalWorkflowAction.BaseAction032 + BaseAction: =>Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction.BaseAction032 -WorkflowInstance: +Symbiote\AdvancedWorkflow\DataObjects\WorkflowInstance: WorkflowInstance01: Title: 'Test WorkflowInstance 01' WorkflowStatus: Paused - Actions: =>WorkflowActionInstance.ActionInstance01,=>WorkflowActionInstance.ActionInstance02,=>WorkflowActionInstance.ActionInstance03 + Actions: + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance01 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance02 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance03 WorkflowInstance02: Title: 'Test WorkflowInstance 02' WorkflowStatus: Paused - Actions: =>WorkflowActionInstance.ActionInstance04,=>WorkflowActionInstance.ActionInstance02,=>WorkflowActionInstance.ActionInstance03 + Actions: + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance04 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance02 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance03 WorkflowInstance03: Title: 'Test WorkflowInstance 03' WorkflowStatus: Paused - Actions: =>WorkflowActionInstance.ActionInstance05,=>WorkflowActionInstance.ActionInstance06,=>WorkflowActionInstance.ActionInstance07,=>WorkflowActionInstance.ActionInstance08 + Actions: + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance05 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance06 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance07 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance08 WorkflowInstance04: Title: 'Test WorkflowInstance 04' WorkflowStatus: Paused - Actions: =>WorkflowActionInstance.ActionInstance09,=>WorkflowActionInstance.ActionInstance10,=>WorkflowActionInstance.ActionInstance11 + Actions: + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance09 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance10 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance11 WorkflowInstance05: Title: 'Test WorkflowInstance 05' WorkflowStatus: Paused - Actions: =>WorkflowActionInstance.ActionInstance12,=>WorkflowActionInstance.ActionInstance13,=>WorkflowActionInstance.ActionInstance14,=>WorkflowActionInstance.ActionInstance15 + Actions: + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance12 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance13 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance14 + - =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance15 WorkflowInstance06: Title: 'Test WorkflowInstance 06' WorkflowStatus: Paused - Actions: =>WorkflowActionInstance.ActionInstance16 - CurrentAction: =>WorkflowActionInstance.ActionInstance16 + Actions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance16 + CurrentAction: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowActionInstance.ActionInstance16 diff --git a/tests/workflowinstancetargets.yml b/tests/workflowinstancetargets.yml index 01395edc..14da48f0 100644 --- a/tests/workflowinstancetargets.yml +++ b/tests/workflowinstancetargets.yml @@ -1,7 +1,7 @@ # # Models DataObject's / SiteTree objects for testing WorkflowInstance::getTarget() # -SiteTree: +SilverStripe\CMS\Model\SiteTree: published-object: ID: 44 Title: 'Page is Published' @@ -11,10 +11,10 @@ SiteTree: Title: 'Page is Draft' Content: 'Draft' -WorkflowInstance: +Symbiote\AdvancedWorkflow\DataObjects\WorkflowInstance: target-is-published: - TargetClass: 'SiteTree' + TargetClass: 'SilverStripe\CMS\Model\SiteTree' TargetID: 44 target-is-draft: - TargetClass: 'SiteTree' + TargetClass: 'SilverStripe\CMS\Model\SiteTree' TargetID: 45 diff --git a/tests/workflowpermissions.yml b/tests/workflowpermissions.yml index bcd20b24..53aceaf4 100644 --- a/tests/workflowpermissions.yml +++ b/tests/workflowpermissions.yml @@ -1,23 +1,23 @@ # # Models the various permission checks on Workflow objects -# -WorkflowTransition: +# +Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition: test-transition-01: Title: 'Test transition 01' - -WorkflowAction: + +Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction: action-no-transitions: Title: 'No transitions' action-with-transitions: Title: 'With transitions' - Transitions: =>WorkflowTransition.test-transition-01 - -WorkflowDefinition: + Transitions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowTransition.test-transition-01 + +Symbiote\AdvancedWorkflow\DataObjects\WorkflowDefinition: no-actions: Title: 'Test definition 01' with-actions: - Title: 'Test definition 02' - Actions: =>WorkflowAction.action-no-transitions + Title: 'Test definition 02' + Actions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction.action-no-transitions with-actions-and-transitions: - Title: 'Test definition 03' - Actions: =>WorkflowAction.action-with-transitions \ No newline at end of file + Title: 'Test definition 03' + Actions: =>Symbiote\AdvancedWorkflow\DataObjects\WorkflowAction.action-with-transitions diff --git a/tests/workflowtemplateimport.yml b/tests/workflowtemplateimport.yml index 4cbb5fed..7796333f 100644 --- a/tests/workflowtemplateimport.yml +++ b/tests/workflowtemplateimport.yml @@ -1,16 +1,16 @@ # # Models the ImportedWorkflowTemplate objects # -ImportedWorkflowTemplate: +Symbiote\AdvancedWorkflow\DataObjects\ImportedWorkflowTemplate: Import01: Name: Test Def Import 01 Filename: workflow-definition-export-1.yml - Content: 'a:1:{s:8:"Injector";a:2:{s:16:"ExportedWorkflow";a:3:{s:5:"class";s:16:"WorkflowTemplate";s:11:"constructor";a:5:{i:0;s:31:"My Workflow 21/02/2014 09-01-29";i:1;s:137:"Exported from osdev.russ.silverstripe.com on 21/02/2014 09-01-29 by Default Admin using SilverStripe versions Framework 3.1.2, CMS 3.1.2";i:2;d:0.20000000000000001;i:3;i:0;i:4;i:1;}s:10:"properties";a:1:{s:9:"structure";a:9:{s:18:"Apply for approval";a:2:{s:4:"type";s:27:"AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:12:"Notify users";}}}s:12:"Notify users";a:2:{s:4:"type";s:25:"NotifyUsersWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:8:"approval";s:8:"Approval";}}}s:8:"Approval";a:2:{s:4:"type";s:28:"SimpleApprovalWorkflowAction";s:11:"transitions";a:2:{i:0;a:1:{s:7:"Approve";s:7:"Publish";}i:1;a:1:{s:6:"Reject";s:14:"Reject changes";}}}s:7:"Publish";a:2:{s:4:"type";s:25:"PublishItemWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:24:"Assign Initiator Publish";}}}s:24:"Assign Initiator Publish";a:2:{s:4:"type";s:27:"AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:24:"Notify Initiator Publish";}}}s:24:"Notify Initiator Publish";a:1:{s:4:"type";s:25:"NotifyUsersWorkflowAction";}s:14:"Reject changes";a:2:{s:4:"type";s:20:"CancelWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:23:"Assign Initiator Cancel";}}}s:23:"Assign Initiator Cancel";a:2:{s:4:"type";s:27:"AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:23:"Notify Initiator Cancel";}}}s:23:"Notify Initiator Cancel";a:1:{s:4:"type";s:25:"NotifyUsersWorkflowAction";}}}}s:15:"WorkflowService";a:1:{s:10:"properties";a:1:{s:9:"templates";a:1:{i:0;s:18:"%$ExportedWorkflow";}}}}}' + Content: 'a:1:{s:35:"SilverStripe\Core\Injector\Injector";a:2:{s:16:"ExportedWorkflow";a:3:{s:5:"class";s:52:"Symbiote\AdvancedWorkflow\Templates\WorkflowTemplate";s:11:"constructor";a:5:{i:0;s:31:"My Workflow 21/02/2014 09-01-29";i:1;s:132:"Exported from osdev.russ.silverstripe.com on 21/02/2014 09-01-29 by Default Admin using SilverStripe versions Framework 4.0.0-beta3";i:2;d:0.20000000000000001;i:3;i:0;i:4;i:1;}s:10:"properties";a:1:{s:9:"structure";a:9:{s:18:"Apply for approval";a:2:{s:4:"type";s:61:"Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:12:"Notify users";}}}s:12:"Notify users";a:2:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:8:"approval";s:8:"Approval";}}}s:8:"Approval";a:2:{s:4:"type";s:62:"Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction";s:11:"transitions";a:2:{i:0;a:1:{s:7:"Approve";s:7:"Publish";}i:1;a:1:{s:6:"Reject";s:14:"Reject changes";}}}s:7:"Publish";a:2:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\PublishItemWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:24:"Assign Initiator Publish";}}}s:24:"Assign Initiator Publish";a:2:{s:4:"type";s:61:"Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:24:"Notify Initiator Publish";}}}s:24:"Notify Initiator Publish";a:1:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction";}s:14:"Reject changes";a:2:{s:4:"type";s:54:"Symbiote\AdvancedWorkflow\Actions\CancelWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:23:"Assign Initiator Cancel";}}}s:23:"Assign Initiator Cancel";a:2:{s:4:"type";s:61:"Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:23:"Notify Initiator Cancel";}}}s:23:"Notify Initiator Cancel";a:1:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction";}}}}s:50:"Symbiote\AdvancedWorkflow\Services\WorkflowService";a:1:{s:10:"properties";a:1:{s:9:"templates";a:1:{i:0;s:18:"%$ExportedWorkflow";}}}}}' Import02: Name: Test Def Import 02 Filename: workflow-definition-export-2.yml - Content: 'a:1:{s:8:"Injector";a:2:{s:16:"ExportedWorkflow";a:3:{s:5:"class";s:16:"WorkflowTemplate";s:11:"constructor";a:5:{i:0;s:33:"My Workflow 1 21/02/2014 09-02-46";i:1;s:137:"Exported from osdev.russ.silverstripe.com on 21/02/2014 09-02-46 by Default Admin using SilverStripe versions Framework 3.1.2, CMS 3.1.2";i:2;d:0.20000000000000001;i:3;i:0;i:4;i:1;}s:10:"properties";a:1:{s:9:"structure";a:9:{s:18:"Apply for approval";a:2:{s:4:"type";s:27:"AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:12:"Notify users";}}}s:12:"Notify users";a:2:{s:4:"type";s:25:"NotifyUsersWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:8:"approval";s:8:"Approval";}}}s:8:"Approval";a:2:{s:4:"type";s:28:"SimpleApprovalWorkflowAction";s:11:"transitions";a:2:{i:0;a:1:{s:7:"Approve";s:7:"Publish";}i:1;a:1:{s:6:"Reject";s:14:"Reject changes";}}}s:7:"Publish";a:2:{s:4:"type";s:25:"PublishItemWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:24:"Assign Initiator Publish";}}}s:24:"Assign Initiator Publish";a:2:{s:4:"type";s:27:"AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:24:"Notify Initiator Publish";}}}s:24:"Notify Initiator Publish";a:1:{s:4:"type";s:25:"NotifyUsersWorkflowAction";}s:14:"Reject changes";a:2:{s:4:"type";s:20:"CancelWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:23:"Assign Initiator Cancel";}}}s:23:"Assign Initiator Cancel";a:2:{s:4:"type";s:27:"AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:23:"Notify Initiator Cancel";}}}s:23:"Notify Initiator Cancel";a:1:{s:4:"type";s:25:"NotifyUsersWorkflowAction";}}}}s:15:"WorkflowService";a:1:{s:10:"properties";a:1:{s:9:"templates";a:1:{i:0;s:18:"%$ExportedWorkflow";}}}}}' + Content: 'a:1:{s:35:"SilverStripe\Core\Injector\Injector";a:2:{s:16:"ExportedWorkflow";a:3:{s:5:"class";s:52:"Symbiote\AdvancedWorkflow\Templates\WorkflowTemplate";s:11:"constructor";a:5:{i:0;s:33:"My Workflow 1 21/02/2014 09-02-46";i:1;s:132:"Exported from osdev.russ.silverstripe.com on 21/02/2014 09-02-46 by Default Admin using SilverStripe versions Framework 4.0.0-beta3";i:2;d:0.20000000000000001;i:3;i:0;i:4;i:1;}s:10:"properties";a:1:{s:9:"structure";a:9:{s:18:"Apply for approval";a:2:{s:4:"type";s:61:"Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:12:"Notify users";}}}s:12:"Notify users";a:2:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:8:"approval";s:8:"Approval";}}}s:8:"Approval";a:2:{s:4:"type";s:62:"Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction";s:11:"transitions";a:2:{i:0;a:1:{s:7:"Approve";s:7:"Publish";}i:1;a:1:{s:6:"Reject";s:14:"Reject changes";}}}s:7:"Publish";a:2:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\PublishItemWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:24:"Assign Initiator Publish";}}}s:24:"Assign Initiator Publish";a:2:{s:4:"type";s:61:"Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:24:"Notify Initiator Publish";}}}s:24:"Notify Initiator Publish";a:1:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction";}s:14:"Reject changes";a:2:{s:4:"type";s:54:"Symbiote\AdvancedWorkflow\Actions\CancelWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:23:"Assign Initiator Cancel";}}}s:23:"Assign Initiator Cancel";a:2:{s:4:"type";s:61:"Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:23:"Notify Initiator Cancel";}}}s:23:"Notify Initiator Cancel";a:1:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction";}}}}s:50:"Symbiote\AdvancedWorkflow\Services\WorkflowService";a:1:{s:10:"properties";a:1:{s:9:"templates";a:1:{i:0;s:18:"%$ExportedWorkflow";}}}}}' Import03: Name: Test Def Import 03 Filename: workflow-definition-export-3.yml - Content: 'a:1:{s:8:"Injector";a:2:{s:16:"ExportedWorkflow";a:3:{s:5:"class";s:16:"WorkflowTemplate";s:11:"constructor";a:5:{i:0;s:33:"My Workflow 2 21/02/2014 09-03-24";i:1;s:137:"Exported from osdev.russ.silverstripe.com on 21/02/2014 09-03-24 by Default Admin using SilverStripe versions Framework 3.1.2, CMS 3.1.2";i:2;d:0.20000000000000001;i:3;i:0;i:4;i:1;}s:10:"properties";a:1:{s:9:"structure";a:9:{s:18:"Apply for approval";a:2:{s:4:"type";s:27:"AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:12:"Notify users";}}}s:12:"Notify users";a:2:{s:4:"type";s:25:"NotifyUsersWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:8:"approval";s:8:"Approval";}}}s:8:"Approval";a:2:{s:4:"type";s:28:"SimpleApprovalWorkflowAction";s:11:"transitions";a:2:{i:0;a:1:{s:7:"Approve";s:7:"Publish";}i:1;a:1:{s:6:"Reject";s:14:"Reject changes";}}}s:7:"Publish";a:2:{s:4:"type";s:25:"PublishItemWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:24:"Assign Initiator Publish";}}}s:24:"Assign Initiator Publish";a:2:{s:4:"type";s:27:"AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:24:"Notify Initiator Publish";}}}s:24:"Notify Initiator Publish";a:1:{s:4:"type";s:25:"NotifyUsersWorkflowAction";}s:14:"Reject changes";a:2:{s:4:"type";s:20:"CancelWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:23:"Assign Initiator Cancel";}}}s:23:"Assign Initiator Cancel";a:2:{s:4:"type";s:27:"AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:23:"Notify Initiator Cancel";}}}s:23:"Notify Initiator Cancel";a:1:{s:4:"type";s:25:"NotifyUsersWorkflowAction";}}}}s:15:"WorkflowService";a:1:{s:10:"properties";a:1:{s:9:"templates";a:1:{i:0;s:18:"%$ExportedWorkflow";}}}}}' \ No newline at end of file + Content: 'a:1:{s:35:"SilverStripe\Core\Injector\Injector";a:2:{s:16:"ExportedWorkflow";a:3:{s:5:"class";s:52:"Symbiote\AdvancedWorkflow\Templates\WorkflowTemplate";s:11:"constructor";a:5:{i:0;s:33:"My Workflow 2 21/02/2014 09-03-24";i:1;s:132:"Exported from osdev.russ.silverstripe.com on 21/02/2014 09-03-24 by Default Admin using SilverStripe versions Framework 4.0.0-beta3";i:2;d:0.20000000000000001;i:3;i:0;i:4;i:1;}s:10:"properties";a:1:{s:9:"structure";a:9:{s:18:"Apply for approval";a:2:{s:4:"type";s:61:"Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:12:"Notify users";}}}s:12:"Notify users";a:2:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:8:"approval";s:8:"Approval";}}}s:8:"Approval";a:2:{s:4:"type";s:62:"Symbiote\AdvancedWorkflow\Actions\SimpleApprovalWorkflowAction";s:11:"transitions";a:2:{i:0;a:1:{s:7:"Approve";s:7:"Publish";}i:1;a:1:{s:6:"Reject";s:14:"Reject changes";}}}s:7:"Publish";a:2:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\PublishItemWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:24:"Assign Initiator Publish";}}}s:24:"Assign Initiator Publish";a:2:{s:4:"type";s:61:"Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:24:"Notify Initiator Publish";}}}s:24:"Notify Initiator Publish";a:1:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction";}s:14:"Reject changes";a:2:{s:4:"type";s:54:"Symbiote\AdvancedWorkflow\Actions\CancelWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"assign";s:23:"Assign Initiator Cancel";}}}s:23:"Assign Initiator Cancel";a:2:{s:4:"type";s:61:"Symbiote\AdvancedWorkflow\Actions\AssignUsersToWorkflowAction";s:11:"transitions";a:1:{i:0;a:1:{s:6:"notify";s:23:"Notify Initiator Cancel";}}}s:23:"Notify Initiator Cancel";a:1:{s:4:"type";s:59:"Symbiote\AdvancedWorkflow\Actions\NotifyUsersWorkflowAction";}}}}s:50:"Symbiote\AdvancedWorkflow\Services\WorkflowService";a:1:{s:10:"properties";a:1:{s:9:"templates";a:1:{i:0;s:18:"%$ExportedWorkflow";}}}}}'