These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Symbiote\AdvancedWorkflow\DataObjects; |
||
4 | |||
5 | use SilverStripe\Control\Controller; |
||
6 | use SilverStripe\Forms\CheckboxSetField; |
||
7 | use SilverStripe\Forms\DropdownField; |
||
8 | use SilverStripe\Forms\FieldGroup; |
||
9 | use SilverStripe\Forms\FieldList; |
||
10 | use SilverStripe\Forms\FormAction; |
||
11 | use SilverStripe\Forms\GridField\GridFieldConfig_RecordEditor; |
||
12 | use SilverStripe\Forms\GridField\GridField; |
||
13 | use SilverStripe\Forms\GridField\GridFieldAddNewButton; |
||
14 | use SilverStripe\Forms\GridField\GridFieldDeleteAction; |
||
15 | use SilverStripe\Forms\GridField\GridFieldEditButton; |
||
16 | use SilverStripe\Forms\GridField\GridFieldViewButton; |
||
17 | use SilverStripe\Forms\GridField\GridFieldDetailForm; |
||
18 | use SilverStripe\Forms\GridField\GridFieldConfig_Base; |
||
19 | use SilverStripe\Forms\LabelField; |
||
20 | use SilverStripe\Forms\LiteralField; |
||
21 | use SilverStripe\Forms\NumericField; |
||
22 | use SilverStripe\Forms\ReadonlyField; |
||
23 | use SilverStripe\Forms\TabSet; |
||
24 | use SilverStripe\Forms\TextareaField; |
||
25 | use SilverStripe\Forms\TextField; |
||
26 | use SilverStripe\Forms\TreeMultiselectField; |
||
27 | use SilverStripe\ORM\DataObject; |
||
28 | use SilverStripe\ORM\DB; |
||
29 | use SilverStripe\Security\Group; |
||
30 | use SilverStripe\Security\Member; |
||
31 | use SilverStripe\Security\Permission; |
||
32 | use SilverStripe\Security\Security; |
||
33 | use Symbiote\AdvancedWorkflow\FormFields\WorkflowField; |
||
34 | use Symbiote\AdvancedWorkflow\Services\WorkflowService; |
||
35 | use Symbiote\QueuedJobs\Services\AbstractQueuedJob; |
||
36 | |||
37 | /** |
||
38 | * An overall definition of a workflow |
||
39 | * |
||
40 | * The workflow definition has a series of steps to it. Each step has a series of possible transitions |
||
41 | * that it can take - the first one that meets certain criteria is followed, which could lead to |
||
42 | * another step. |
||
43 | * |
||
44 | * A step is either manual or automatic; an example 'manual' step would be requiring a person to review |
||
45 | * a document. An automatic step might be to email a group of people, or to publish documents. |
||
46 | * Basically, a manual step requires the interaction of someone to pick which action to take, an automatic |
||
47 | * step will automatically determine what to do once it has finished. |
||
48 | * |
||
49 | * @author [email protected] |
||
50 | * @license BSD License (http://silverstripe.org/bsd-license/) |
||
51 | * @package advancedworkflow |
||
52 | */ |
||
53 | class WorkflowDefinition extends DataObject |
||
54 | { |
||
55 | private static $db = array( |
||
56 | 'Title' => 'Varchar(128)', |
||
57 | 'Description' => 'Text', |
||
58 | 'Template' => 'Varchar', |
||
59 | 'TemplateVersion' => 'Varchar', |
||
60 | 'RemindDays' => 'Int', |
||
61 | 'Sort' => 'Int', |
||
62 | 'InitialActionButtonText' => 'Varchar', |
||
63 | ); |
||
64 | |||
65 | private static $default_sort = 'Sort'; |
||
66 | |||
67 | private static $has_many = array( |
||
68 | 'Actions' => WorkflowAction::class, |
||
69 | 'Instances' => WorkflowInstance::class |
||
70 | ); |
||
71 | |||
72 | /** |
||
73 | * By default, a workflow definition is bound to a particular set of users or groups. |
||
74 | * |
||
75 | * This is covered across to the workflow instance - it is up to subsequent |
||
76 | * workflow actions to change this if needbe. |
||
77 | * |
||
78 | * @var array |
||
79 | */ |
||
80 | private static $many_many = array( |
||
81 | 'Users' => Member::class, |
||
82 | 'Groups' => Group::class, |
||
83 | ); |
||
84 | |||
85 | private static $icon = 'advancedworkflow/images/definition.png'; |
||
0 ignored issues
–
show
|
|||
86 | |||
87 | public static $default_workflow_title_base = 'My Workflow'; |
||
88 | |||
89 | public static $workflow_defs = array(); |
||
90 | |||
91 | private static $dependencies = array( |
||
92 | 'workflowService' => '%$' . WorkflowService::class, |
||
93 | ); |
||
94 | |||
95 | private static $table_name = 'WorkflowDefinition'; |
||
96 | |||
97 | /** |
||
98 | * @var WorkflowService |
||
99 | */ |
||
100 | public $workflowService; |
||
101 | |||
102 | /** |
||
103 | * Gets the action that first triggers off the workflow |
||
104 | * |
||
105 | * @return WorkflowAction |
||
106 | */ |
||
107 | public function getInitialAction() |
||
108 | { |
||
109 | if ($actions = $this->Actions()) { |
||
110 | return $actions->First(); |
||
111 | } |
||
112 | } |
||
113 | |||
114 | /** |
||
115 | * Ensure a sort value is set and we get a useable initial workflow title. |
||
116 | */ |
||
117 | public function onBeforeWrite() |
||
118 | { |
||
119 | if (!$this->Sort) { |
||
120 | $this->Sort = DB::query('SELECT MAX("Sort") + 1 FROM "WorkflowDefinition"')->value(); |
||
121 | } |
||
122 | if (!$this->ID && !$this->Title) { |
||
123 | $this->Title = $this->getDefaultWorkflowTitle(); |
||
124 | } |
||
125 | |||
126 | parent::onBeforeWrite(); |
||
127 | } |
||
128 | |||
129 | /** |
||
130 | * After we've been written, check whether we've got a template and to then |
||
131 | * create the relevant actions etc. |
||
132 | */ |
||
133 | public function onAfterWrite() |
||
134 | { |
||
135 | parent::onAfterWrite(); |
||
136 | |||
137 | // Request via ImportForm where TemplateVersion is already set, so unset it |
||
138 | $posted = Controller::curr()->getRequest()->postVars(); |
||
139 | if (isset($posted['_CsvFile']) && $this->TemplateVersion) { |
||
140 | $this->TemplateVersion = null; |
||
141 | } |
||
142 | if ($this->numChildren() == 0 && $this->Template && !$this->TemplateVersion) { |
||
143 | $this->workflowService->defineFromTemplate($this, $this->Template); |
||
144 | } |
||
145 | } |
||
146 | |||
147 | /** |
||
148 | * Ensure all WorkflowDefinition relations are removed on delete. If we don't do this, |
||
149 | * we see issues with targets previously under the control of a now-deleted workflow, |
||
150 | * becoming stuck, even if a new workflow is subsequently assigned to it. |
||
151 | * |
||
152 | * @return null |
||
153 | */ |
||
154 | public function onBeforeDelete() |
||
155 | { |
||
156 | parent::onBeforeDelete(); |
||
157 | |||
158 | // Delete related import |
||
159 | $this->deleteRelatedImport(); |
||
160 | |||
161 | // Reset/unlink related HasMany|ManyMany relations and their orphaned objects |
||
162 | $this->removeRelatedHasLists(); |
||
163 | } |
||
164 | |||
165 | /** |
||
166 | * Removes User+Group relations from this object as well as WorkflowAction relations. |
||
167 | * When a WorkflowAction is deleted, its own relations are also removed: |
||
168 | * - WorkflowInstance |
||
169 | * - WorkflowTransition |
||
170 | * @see WorkflowAction::onAfterDelete() |
||
171 | * |
||
172 | * @return void |
||
173 | */ |
||
174 | private function removeRelatedHasLists() |
||
175 | { |
||
176 | $this->Users()->removeAll(); |
||
177 | $this->Groups()->removeAll(); |
||
178 | $this->Actions()->each(function ($action) { |
||
179 | if ($orphan = DataObject::get_by_id(WorkflowAction::class, $action->ID)) { |
||
180 | $orphan->delete(); |
||
181 | } |
||
182 | }); |
||
183 | } |
||
184 | |||
185 | /** |
||
186 | * |
||
187 | * Deletes related ImportedWorkflowTemplate objects. |
||
188 | * |
||
189 | * @return void |
||
190 | */ |
||
191 | private function deleteRelatedImport() |
||
192 | { |
||
193 | if ($import = DataObject::get(ImportedWorkflowTemplate::class)->filter('DefinitionID', $this->ID)->first()) { |
||
194 | $import->delete(); |
||
195 | } |
||
196 | } |
||
197 | |||
198 | /** |
||
199 | * @return int |
||
200 | */ |
||
201 | public function numChildren() |
||
202 | { |
||
203 | return $this->Actions()->count(); |
||
204 | } |
||
205 | |||
206 | public function fieldLabels($includerelations = true) |
||
207 | { |
||
208 | $labels = parent::fieldLabels($includerelations); |
||
209 | $labels['Title'] = _t('WorkflowDefinition.TITLE', 'Title'); |
||
210 | $labels['Description'] = _t('WorkflowDefinition.DESCRIPTION', 'Description'); |
||
211 | $labels['Template'] = _t('WorkflowDefinition.TEMPLATE_NAME', 'Source Template'); |
||
212 | $labels['TemplateVersion'] = _t('WorkflowDefinition.TEMPLATE_VERSION', 'Template Version'); |
||
213 | |||
214 | return $labels; |
||
215 | } |
||
216 | |||
217 | public function getCMSFields() |
||
218 | { |
||
219 | |||
220 | $cmsUsers = Member::mapInCMSGroups(); |
||
221 | |||
222 | $fields = new FieldList(new TabSet('Root')); |
||
223 | |||
224 | $fields->addFieldToTab('Root.Main', new TextField('Title', $this->fieldLabel('Title'))); |
||
225 | $fields->addFieldToTab('Root.Main', new TextareaField('Description', $this->fieldLabel('Description'))); |
||
226 | $fields->addFieldToTab('Root.Main', TextField::create( |
||
227 | 'InitialActionButtonText', |
||
228 | _t('WorkflowDefinition.INITIAL_ACTION_BUTTON_TEXT', 'Initial Action Button Text') |
||
229 | )); |
||
230 | if ($this->ID) { |
||
231 | $fields->addFieldToTab('Root.Main', new CheckboxSetField('Users', _t('WorkflowDefinition.USERS', 'Users'), $cmsUsers)); |
||
232 | $fields->addFieldToTab('Root.Main', new TreeMultiselectField('Groups', _t('WorkflowDefinition.GROUPS', 'Groups'), Group::class)); |
||
233 | } |
||
234 | |||
235 | if (class_exists(AbstractQueuedJob::class)) { |
||
236 | $fields->addFieldToTab( |
||
237 | 'Root.Main', |
||
238 | NumericField::create( |
||
239 | 'ReminderEmail', |
||
240 | _t('WorkflowDefinition.REMINDEREMAIL', 'Reminder Email') |
||
241 | )->setDescription(_t( |
||
242 | __CLASS__ . '.ReminderEmailDescription', |
||
243 | 'Send reminder email after the specified number of days without action.' |
||
244 | )) |
||
245 | ); |
||
246 | } |
||
247 | |||
248 | if ($this->ID) { |
||
249 | if ($this->Template) { |
||
250 | $template = $this->workflowService->getNamedTemplate($this->Template); |
||
251 | $fields->addFieldToTab('Root.Main', new ReadonlyField('Template', $this->fieldLabel('Template'), $this->Template)); |
||
252 | $fields->addFieldToTab('Root.Main', new ReadonlyField('TemplateDesc', _t('WorkflowDefinition.TEMPLATE_INFO', 'Template Info'), $template ? $template->getDescription() : '')); |
||
253 | $fields->addFieldToTab('Root.Main', $tv = new ReadonlyField('TemplateVersion', $this->fieldLabel('TemplateVersion'))); |
||
254 | $tv->setRightTitle(sprintf(_t('WorkflowDefinition.LATEST_VERSION', 'Latest version is %s'), $template ? $template->getVersion() : '')); |
||
255 | } |
||
256 | |||
257 | $fields->addFieldToTab('Root.Main', new WorkflowField( |
||
258 | 'Workflow', |
||
259 | _t('WorkflowDefinition.WORKFLOW', 'Workflow'), |
||
260 | $this |
||
261 | )); |
||
262 | } else { |
||
263 | // add in the 'template' info |
||
264 | $templates = $this->workflowService->getTemplates(); |
||
265 | |||
266 | if (is_array($templates)) { |
||
267 | $items = array('' => ''); |
||
268 | foreach ($templates as $template) { |
||
269 | $items[$template->getName()] = $template->getName(); |
||
270 | } |
||
271 | $templates = array_combine(array_keys($templates), array_keys($templates)); |
||
272 | |||
273 | $fields->addFieldToTab('Root.Main', $dd = new DropdownField('Template', _t('WorkflowDefinition.CHOOSE_TEMPLATE', 'Choose template (optional)'), $items)); |
||
274 | $dd->setRightTitle(_t('WorkflowDefinition.CHOOSE_TEMPLATE_RIGHT', 'If set, this workflow definition will be automatically updated if the template is changed')); |
||
275 | } |
||
276 | |||
277 | /* |
||
278 | * Uncomment to allow pre-uploaded exports to appear in a new DropdownField. |
||
279 | * |
||
280 | * $import = singleton('WorkflowDefinitionImporter')->getImportedWorkflows(); |
||
281 | * if (is_array($import)) { |
||
282 | * $_imports = array('' => ''); |
||
283 | * foreach ($imports as $import) { |
||
284 | * $_imports[$import->getName()] = $import->getName(); |
||
285 | * } |
||
286 | * $imports = array_combine(array_keys($_imports), array_keys($_imports)); |
||
287 | * $fields->addFieldToTab('Root.Main', new DropdownField('Import', _t('WorkflowDefinition.CHOOSE_IMPORT', 'Choose import (optional)'), $imports)); |
||
288 | * } |
||
289 | */ |
||
290 | |||
291 | $message = _t( |
||
292 | 'WorkflowDefinition.ADDAFTERSAVING', |
||
293 | 'You can add workflow steps after you save for the first time.' |
||
294 | ); |
||
295 | $fields->addFieldToTab('Root.Main', new LiteralField( |
||
296 | 'AddAfterSaving', |
||
297 | "<p class='message notice'>$message</p>" |
||
298 | )); |
||
299 | } |
||
300 | |||
301 | if ($this->ID && Permission::check('VIEW_ACTIVE_WORKFLOWS')) { |
||
302 | $active = $this->Instances()->filter(array( |
||
303 | 'WorkflowStatus' => array('Active', 'Paused') |
||
304 | )); |
||
305 | |||
306 | $active = new GridField( |
||
307 | 'Active', |
||
308 | _t('WorkflowDefinition.WORKFLOWACTIVEIINSTANCES', 'Active Workflow Instances'), |
||
309 | $active, |
||
310 | new GridFieldConfig_RecordEditor() |
||
311 | ); |
||
312 | |||
313 | $active->getConfig()->removeComponentsByType(GridFieldAddNewButton::class); |
||
314 | $active->getConfig()->removeComponentsByType(GridFieldDeleteAction::class); |
||
315 | |||
316 | if (!Permission::check('REASSIGN_ACTIVE_WORKFLOWS')) { |
||
317 | $active->getConfig()->removeComponentsByType(GridFieldEditButton::class); |
||
318 | $active->getConfig()->addComponent(new GridFieldViewButton()); |
||
319 | $active->getConfig()->addComponent(new GridFieldDetailForm()); |
||
320 | } |
||
321 | |||
322 | $completed = $this->Instances()->filter(array( |
||
323 | 'WorkflowStatus' => array('Complete', 'Cancelled') |
||
324 | )); |
||
325 | |||
326 | $config = new GridFieldConfig_Base(); |
||
327 | $config->addComponent(new GridFieldEditButton()); |
||
328 | $config->addComponent(new GridFieldDetailForm()); |
||
329 | |||
330 | $completed = new GridField( |
||
331 | 'Completed', |
||
332 | _t('WorkflowDefinition.WORKFLOWCOMPLETEDIINSTANCES', 'Completed Workflow Instances'), |
||
333 | $completed, |
||
334 | $config |
||
335 | ); |
||
336 | |||
337 | $fields->findOrMakeTab( |
||
338 | 'Root.Active', |
||
339 | _t('WorkflowEmbargoExpiryExtension.ActiveWorkflowStateTitle', 'Active') |
||
340 | ); |
||
341 | $fields->addFieldToTab('Root.Active', $active); |
||
342 | |||
343 | $fields->findOrMakeTab( |
||
344 | 'Root.Completed', |
||
345 | _t('WorkflowEmbargoExpiryExtension.CompletedWorkflowStateTitle', 'Completed') |
||
346 | ); |
||
347 | $fields->addFieldToTab('Root.Completed', $completed); |
||
348 | } |
||
349 | |||
350 | $this->extend('updateCMSFields', $fields); |
||
351 | |||
352 | return $fields; |
||
353 | } |
||
354 | |||
355 | public function updateAdminActions($actions) |
||
356 | { |
||
357 | if ($this->Template) { |
||
358 | $template = $this->workflowService->getNamedTemplate($this->Template); |
||
359 | if ($template && $this->TemplateVersion != $template->getVersion()) { |
||
360 | $label = sprintf(_t('WorkflowDefinition.UPDATE_FROM_TEMLPATE', 'Update to latest template version (%s)'), $template->getVersion()); |
||
361 | $actions->push($action = FormAction::create('updatetemplateversion', $label)); |
||
362 | } |
||
363 | } |
||
364 | } |
||
365 | |||
366 | public function updateFromTemplate() |
||
367 | { |
||
368 | if ($this->Template) { |
||
369 | $template = $this->workflowService->getNamedTemplate($this->Template); |
||
370 | $template->updateDefinition($this); |
||
371 | } |
||
372 | } |
||
373 | |||
374 | /** |
||
375 | * If a workflow-title doesn't already exist, we automatically create a suitable default title |
||
376 | * when users attempt to create title-less workflow definitions or upload/create Workflows that would |
||
377 | * otherwise have the same name. |
||
378 | * |
||
379 | * @return string |
||
380 | * @todo Filter query on current-user's workflows. Avoids confusion when other users may already have 'My Workflow 1' |
||
381 | * and user sees 'My Workflow 2' |
||
382 | */ |
||
383 | public function getDefaultWorkflowTitle() |
||
384 | { |
||
385 | // Where is the title coming from that we wish to test? |
||
386 | $incomingTitle = $this->incomingTitle(); |
||
387 | $defs = WorkflowDefinition::get()->map()->toArray(); |
||
388 | $tmp = array(); |
||
389 | |||
390 | foreach ($defs as $def) { |
||
391 | $parts = preg_split("#\s#", $def, -1, PREG_SPLIT_NO_EMPTY); |
||
392 | $lastPart = array_pop($parts); |
||
393 | $match = implode(' ', $parts); |
||
394 | // @todo do all this in one preg_match_all() call |
||
395 | if (preg_match("#$match#", $incomingTitle)) { |
||
396 | // @todo use a simple incrementer?? |
||
397 | if ($incomingTitle.' '.$lastPart == $def) { |
||
398 | array_push($tmp, $lastPart); |
||
399 | } |
||
400 | } |
||
401 | } |
||
402 | |||
403 | $incr = 1; |
||
404 | if (count($tmp)) { |
||
405 | sort($tmp, SORT_NUMERIC); |
||
406 | $incr = (int)end($tmp)+1; |
||
407 | } |
||
408 | return $incomingTitle.' '.$incr; |
||
409 | } |
||
410 | |||
411 | /** |
||
412 | * Return the workflow definition title according to the source |
||
413 | * |
||
414 | * @return string |
||
415 | */ |
||
416 | public function incomingTitle() |
||
417 | { |
||
418 | $req = Controller::curr()->getRequest(); |
||
419 | if (isset($req['_CsvFile']['name']) && !empty($req['_CsvFile']['name'])) { |
||
420 | $import = ImportedWorkflowTemplate::get()->filter('Filename', $req['_CsvFile']['name'])->first(); |
||
421 | $incomingTitle = $import->Name; |
||
422 | } elseif (isset($req['Template']) && !empty($req['Template'])) { |
||
423 | $incomingTitle = $req['Template']; |
||
424 | } elseif (isset($req['Title']) && !empty($req['Title'])) { |
||
425 | $incomingTitle = $req['Title']; |
||
426 | } else { |
||
427 | $incomingTitle = self::$default_workflow_title_base; |
||
428 | } |
||
429 | return $incomingTitle; |
||
430 | } |
||
431 | |||
432 | /** |
||
433 | * Determines if target can be published directly when no workflow has started yet |
||
434 | * Opens extension hook to allow an extension to determine if this is allowed as well |
||
435 | * |
||
436 | * By default returns false |
||
437 | * |
||
438 | * @param $member |
||
439 | * @param $target |
||
440 | * @return Boolean |
||
441 | */ |
||
442 | public function canWorkflowPublish($member, $target) |
||
443 | { |
||
444 | $publish = $this->extendedCan('canWorkflowPublish', $member, $target); |
||
445 | |||
446 | if (is_null($publish)) { |
||
447 | return false; |
||
448 | } |
||
449 | return $publish; |
||
450 | } |
||
451 | |||
452 | /** |
||
453 | * |
||
454 | * @param Member $member |
||
455 | * @param array $context |
||
456 | * @return bool |
||
457 | */ |
||
458 | public function canCreate($member = null, $context = array()) |
||
459 | { |
||
460 | if (is_null($member)) { |
||
461 | if (!Security::getCurrentUser()) { |
||
462 | return false; |
||
463 | } |
||
464 | $member = Security::getCurrentUser(); |
||
465 | } |
||
466 | return Permission::checkMember($member, 'CREATE_WORKFLOW'); |
||
467 | } |
||
468 | |||
469 | /** |
||
470 | * |
||
471 | * @param Member $member |
||
472 | * @return boolean |
||
473 | */ |
||
474 | public function canView($member = null) |
||
475 | { |
||
476 | return $this->userHasAccess($member); |
||
477 | } |
||
478 | |||
479 | /** |
||
480 | * |
||
481 | * @param Member $member |
||
482 | * @return boolean |
||
483 | */ |
||
484 | public function canEdit($member = null) |
||
485 | { |
||
486 | return $this->canCreate($member); |
||
487 | } |
||
488 | |||
489 | /** |
||
490 | * |
||
491 | * @param Member $member |
||
492 | * @return boolean |
||
493 | * @see {@link $this->onBeforeDelete()} |
||
494 | */ |
||
495 | View Code Duplication | public function canDelete($member = null) |
|
496 | { |
||
497 | if (!$member) { |
||
498 | if (!Security::getCurrentUser()) { |
||
499 | return false; |
||
500 | } |
||
501 | $member = Security::getCurrentUser(); |
||
502 | } |
||
503 | |||
504 | if (Permission::checkMember($member, 'ADMIN')) { |
||
505 | return true; |
||
506 | } |
||
507 | |||
508 | /* |
||
509 | * DELETE_WORKFLOW should trump all other canDelete() return values on |
||
510 | * related objects. |
||
511 | * @see {@link $this->onBeforeDelete()} |
||
512 | */ |
||
513 | return Permission::checkMember($member, 'DELETE_WORKFLOW'); |
||
514 | } |
||
515 | |||
516 | /** |
||
517 | * Checks whether the passed user is able to view this ModelAdmin |
||
518 | * |
||
519 | * @param Member $member |
||
520 | * @return bool |
||
521 | */ |
||
522 | View Code Duplication | protected function userHasAccess($member) |
|
523 | { |
||
524 | if (!$member) { |
||
525 | if (!Security::getCurrentUser()) { |
||
526 | return false; |
||
527 | } |
||
528 | $member = Security::getCurrentUser(); |
||
529 | } |
||
530 | |||
531 | if (Permission::checkMember($member, "VIEW_ACTIVE_WORKFLOWS")) { |
||
532 | return true; |
||
533 | } |
||
534 | } |
||
535 | } |
||
536 |
This check marks private properties in classes that are never used. Those properties can be removed.