Completed
Push — master ( 30e1bf...007fe5 )
by Marcus
16s queued 11s
created

code/dataobjects/WorkflowInstance.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * A WorkflowInstance is created whenever a user 'starts' a workflow.
4
 *
5
 * This 'start' is triggered automatically when the user clicks the relevant
6
 * button (eg 'apply for approval'). This creates a standalone object
7
 * that maintains the state of the workflow process.
8
 *
9
 * @method WorkflowDefinition Definition()
10
 * @method WorkflowActionInstance CurrentAction()
11
 * @method Member Initiator()
12
 *
13
 * @author  [email protected]
14
 * @license BSD License (http://silverstripe.org/bsd-license/)
15
 * @package advancedworkflow
16
 */
17
class WorkflowInstance extends DataObject {
18
19
	private static $db = array(
20
		'Title'				=> 'Varchar(128)',
21
		'WorkflowStatus'	=> "Enum('Active,Paused,Complete,Cancelled','Active')",
22
		'TargetClass'		=> 'Varchar(64)',
23
		'TargetID'			=> 'Int',
24
	);
25
26
	private static $has_one = array(
27
		'Definition'    => 'WorkflowDefinition',
28
		'CurrentAction' => 'WorkflowActionInstance',
29
		'Initiator'		=> 'Member',
30
	);
31
32
	private static $has_many = array(
33
		'Actions'		=> 'WorkflowActionInstance',
34
	);
35
36
	/**
37
	 * The list of users who are responsible for performing the current WorkflowAction
38
	 *
39
	 * @var array
40
	 */
41
	private static $many_many = array(
42
		'Users'			=> 'Member',
43
		'Groups'		=> 'Group'
44
	);
45
46
	private static $summary_fields = array(
47
		'Title',
48
		'WorkflowStatus',
49
		'Created'
50
	);
51
52
	/**
53
	 * If set to true, actions that cannot be executed by the user will not show
54
	 * on the frontend (just like the backend). 
55
	 *
56
	 * @var boolean
57
	 */
58
	private static $hide_disabled_actions_on_frontend = false;
59
60
	/**
61
	 * Get the CMS view of the instance. This is used to display the log of
62
	 * this workflow, and options to reassign if the workflow hasn't been
63
	 * finished yet
64
	 *
65
	 * @return \FieldList
66
	 */
67
	public function getCMSFields() {
68
		$fields = new FieldList();
69
70
		if (Permission::check('REASSIGN_ACTIVE_WORKFLOWS')) {
71
			if ($this->WorkflowStatus == 'Paused' || $this->WorkflowStatus == 'Active') {
72
				$cmsUsers = Member::mapInCMSGroups();
73
74
				$fields->push(new HiddenField('DirectUpdate', '', 1));
75
76
				$fields->push(new HeaderField('InstanceReassignHeader',_t('WorkflowInstance.REASSIGN_HEADER', 'Reassign workflow')));
77
				$fields->push(new CheckboxSetField('Users', _t('WorkflowDefinition.USERS', 'Users'), $cmsUsers));
78
				$fields->push(new TreeMultiselectField('Groups', _t('WorkflowDefinition.GROUPS', 'Groups'), 'Group'));
79
80
			}
81
		}
82
83
		if ($this->canEdit()) {
84
			$action = $this->CurrentAction();
85
			if ($action->exists()) {
86
				$actionFields = $this->getWorkflowFields();
87
				$fields->merge($actionFields);
88
89
				$transitions = $action->getValidTransitions();
90
				if ($transitions) {
91
					$fields->replaceField('TransitionID', DropdownField::create("TransitionID", "Next action", $transitions->map()));
92
				}
93
			}
94
		}
95
96
		$items = WorkflowActionInstance::get()->filter(array(
97
			'Finished'		=> 1,
98
			'WorkflowID'	=> $this->ID
99
		));
100
101
		$grid = new GridField(
102
			'Actions',
103
			_t('WorkflowInstance.ActionLogTitle','Log'),
104
			$items
105
		);
106
107
		$fields->push($grid);
108
109
		return $fields;
110
	}
111
112
	public function fieldLabels($includerelations = true) {
113
		$labels = parent::fieldLabels($includerelations);
114
		$labels['Title'] = _t('WorkflowInstance.TitleLabel', 'Title');
115
		$labels['WorkflowStatus'] = _t('WorkflowInstance.WorkflowStatusLabel', 'Workflow Status');
116
		$labels['TargetClass'] = _t('WorkflowInstance.TargetClassLabel', 'Target Class');
117
		$labels['TargetID'] = _t('WorkflowInstance.TargetIDLabel', 'Target');
118
119
		return $labels;
120
	}
121
122
	/**
123
	 * See if we've been saved in context of managing the workflow directly
124
	 */
125
	public function onBeforeWrite() {
126
		parent::onBeforeWrite();
127
128
		$vars = $this->record;
129
130
		if (isset($vars['DirectUpdate'])) {
131
			// Unset now so that we don't end up in an infinite loop!
132
			unset($this->record['DirectUpdate']);
133
			$this->updateWorkflow($vars);
134
		}
135
	}
136
137
	/**
138
	 * Update the current state of the workflow
139
	 *
140
	 * Typically, this is triggered by someone modifiying the workflow instance via the modeladmin form
141
	 * side of things when administering things, such as re-assigning or manually approving a stuck workflow
142
	 *
143
	 * Note that this is VERY similar to AdvancedWorkflowExtension::updateworkflow
144
	 * but without the formy bits. These two implementations should PROBABLY
145
	 * be merged
146
	 *
147
	 * @todo refactor with AdvancedWorkflowExtension
148
	 *
149
	 * @param type $data
150
	 * @return
151
	 */
152
	public function updateWorkflow($data) {
153
		$action = $this->CurrentAction();
154
155
		if (!$this->getTarget() || !$this->getTarget()->canEditWorkflow()) {
156
			return;
157
		}
158
159
		$allowedFields = $this->getWorkflowFields()->saveableFields();
160
		unset($allowedFields['TransitionID']);
161
		foreach ($allowedFields as $field) {
162
			$fieldName = $field->getName();
163
			$action->$fieldName = $data[$fieldName];
164
		}
165
		$action->write();
166
167
		$svc = singleton('WorkflowService');
168
		if (isset($data['TransitionID']) && $data['TransitionID']) {
169
			$svc->executeTransition($this->getTarget(), $data['TransitionID']);
170
		} else {
171
			// otherwise, just try to execute the current workflow to see if it
172
			// can now proceed based on user input
173
			$this->execute();
174
		}
175
	}
176
177
	/**
178
	 * Get the target-object that this WorkflowInstance "points" to.
179
	 *
180
	 * Workflows are not restricted to being active on SiteTree objects,
181
	 * so we need to account for being attached to anything.
182
	 *
183
	 * Sets Versioned::set_reading_mode() to allow fetching of Draft _and_ Published
184
	 * content.
185
	 *
186
	 * @return (null | DataObject)
187
	 */
188
	public function getTarget() {
189
		if($this->TargetID && $this->TargetClass) {
190
			$versionable = Injector::inst()->get($this->TargetClass)->has_extension('Versioned');
191
			if($versionable) {
192
				Versioned::set_reading_mode("Stage.Stage");
193
			}
194
195
			// Default
196
			return DataObject::get_by_id($this->TargetClass, $this->TargetID);
197
		}
198
	}
199
200
	/**
201
	 *
202
	 * @see {@link {$this->getTarget()}
203
	 * @return (null | DataObject)
204
	 */
205
	public function Target() {
206
		return $this->getTarget();
207
	}
208
209
	/**
210
	 * Start a workflow based on a particular definition for a particular object.
211
	 *
212
	 * The object is optional; if not specified, it is assumed that this workflow
213
	 * is simply a task based checklist type of workflow.
214
	 *
215
	 * @param WorkflowDefinition $definition
216
	 * @param DataObject $for
217
	 */
218
	public function beginWorkflow(WorkflowDefinition $definition, DataObject $for=null) {
219
		if(!$this->ID) {
220
			$this->write();
221
		}
222
223
		if ($for && ($for->hasExtension('WorkflowApplicable') || $for->hasExtension('FileWorkflowApplicable'))) {
224
			$this->TargetClass = ClassInfo::baseDataClass($for);
225
			$this->TargetID = $for->ID;
226
		}
227
228
		// lets create the first WorkflowActionInstance.
229
		$action = $definition->getInitialAction()->getInstanceForWorkflow();
230
		$action->WorkflowID   = $this->ID;
231
		$action->write();
232
233
		$title = $for && $for->hasField('Title') ?
234
				sprintf(_t('WorkflowInstance.TITLE_FOR_DO', '%s - %s'), $definition->Title, $for->Title) :
235
				sprintf(_t('WorkflowInstance.TITLE_STUB', 'Instance #%s of %s'), $this->ID, $definition->Title);
236
237
		$this->Title		   = $title;
238
		$this->DefinitionID    = $definition->ID;
239
		$this->CurrentActionID = $action->ID;
240
		$this->InitiatorID     = Member::currentUserID();
241
		$this->write();
242
243
		$this->Users()->addMany($definition->Users());
244
		$this->Groups()->addMany($definition->Groups());
245
	}
246
247
	/**
248
	 * Execute this workflow. In rare cases this will actually execute all actions,
249
	 * but typically, it will stop and wait for the user to input something
250
	 *
251
	 * The basic process is to get the current action, and see whether it has been finished
252
	 * by some process, if not it attempts to execute it.
253
	 *
254
	 * If it has been finished, we check to see if there's some transitions to follow. If there's
255
	 * only one transition, then we execute that immediately.
256
	 *
257
	 * If there's multiple transitions, we just stop and wait for the user to manually
258
	 * trigger a transition.
259
	 *
260
	 * If there's no transitions, we make the assumption that we've finished the workflow and
261
	 * mark it as such.
262
	 *
263
	 *
264
	 */
265
	public function execute() {
266
		if (!$this->CurrentActionID) {
267
			throw new Exception(
268
				sprintf(_t('WorkflowInstance.EXECUTE_EXCEPTION', 'Attempted to start an invalid workflow instance #%s!'), $this->ID)
269
			);
270
		}
271
272
		$action     = $this->CurrentAction();
273
		$transition = false;
274
275
		// if the action has already finished, it means it has either multiple (or no
276
		// transitions at the time), so a subsequent check should be run.
277
		if($action->Finished) {
278
			$transition = $this->checkTransitions($action);
279
		} else {
280
			$result = $action->BaseAction()->execute($this);
281
282
			// if the action was successful, then the action has finished running and
283
			// next transition should be run (if only one).
284
			// input.
285
			if($result) {
286
				$action->MemberID = Member::currentUserID();
287
				$action->Finished = true;
288
				$action->write();
289
				$transition = $this->checkTransitions($action);
290
			}
291
		}
292
293
		// if the action finished, and there's only one available transition then
294
		// move onto that step - otherwise check if the workflow has finished.
295
		if($transition) {
296
			$this->performTransition($transition);
297
		} else {
298
			// see if there are any transitions available, even if they are not valid.
299
			if($action->Finished && !count($action->BaseAction()->Transitions())) {
300
				$this->WorkflowStatus  = 'Complete';
301
				$this->CurrentActionID = 0;
302
			} else {
303
				$this->WorkflowStatus = 'Paused';
304
			}
305
306
			$this->write();
307
		}
308
	}
309
310
	/**
311
	 * Evaluate all the transitions of an action and determine whether we should
312
	 * follow any of them yet.
313
	 *
314
	 * @param  WorkflowActionInstance $action
315
	 * @return WorkflowTransition
316
	 */
317
	protected function checkTransitions(WorkflowActionInstance $action) {
318
		$transitions = $action->getValidTransitions();
319
		// if there's JUST ONE transition, then we need should
320
		// immediately follow it.
321
		if ($transitions && $transitions->count() == 1) {
322
			return $transitions->First();
323
		}
324
	}
325
326
	/**
327
	 * Transitions a workflow to the next step defined by the given transition.
328
	 *
329
	 * After transitioning, the action is 'executed', and next steps
330
	 * determined.
331
	 *
332
	 * @param WorkflowTransition $transition
333
	 */
334
	public function performTransition(WorkflowTransition $transition) {
335
		// first make sure that the transition is valid to execute!
336
		$action          = $this->CurrentAction();
337
		$allTransitions  = $action->BaseAction()->Transitions();
338
339
		$valid = $allTransitions->find('ID', $transition->ID);
340
		if (!$valid) {
341
			throw new Exception (
342
				sprintf(_t('WorkflowInstance.WORKFLOW_TRANSITION_EXCEPTION', 'Invalid transition state for action #%s'), $action->ID)
343
			);
344
345
		}
346
347
		$action->actionComplete($transition);
348
349
		$definition = DataObject::get_by_id('WorkflowAction', $transition->NextActionID);
350
		$action = $definition->getInstanceForWorkflow();
351
		$action->WorkflowID   = $this->ID;
352
		$action->write();
353
354
		$this->CurrentActionID = $action->ID;
355
		$this->write();
356
		$this->components = array(); // manually clear the has_one cache
357
358
		$action->actionStart($transition);
359
360
		$transition->extend('onTransition');
361
		$this->execute();
362
	}
363
364
	/**
365
	 * Returns a list of all Members that are assigned to this instance, either directly or via a group.
366
	 *
367
	 * @todo   This could be made more efficient.
368
	 * @return ArrayList
369
	 */
370 View Code Duplication
	public function getAssignedMembers() {
371
		$list    = new ArrayList();
372
		$groups  = $this->Groups();
373
374
		$list->merge($this->Users());
375
376
		foreach($groups as $group) {
377
			$list->merge($group->Members());
378
		}
379
380
		$list->removeDuplicates();
381
		return $list;
382
	}
383
384
	/**
385
	 *
386
	 * @param \Member $member
387
	 * @return boolean
388
	 */
389
	public function canView($member=null) {
390
		$extended = $this->extendedCan(__FUNCTION__, $member);
391
		if($extended !== null) return $extended;
392
393
		$hasAccess = $this->userHasAccess($member);
394
		/*
395
		 * If the next action is AssignUsersToWorkflowAction, execute() resets all user+group relations.
396
		 * Therefore current user no-longer has permission to view this WorkflowInstance in PendingObjects Gridfield, even though;
397
		 * - She had permissions granted via the workflow definition to run the preceeding Action that took her here.
398
		 */
399
		if(!$hasAccess) {
400
			if($this->getMostRecentActionForUser($member)) {
401
				return true;
402
			}
403
		}
404
		return $hasAccess;
405
	}
406
407
	/**
408
	 *
409
	 * @param \Member $member
410
	 * @return boolean
411
	 */
412
	public function canEdit($member = null) {
413
		$extended = $this->extendedCan(__FUNCTION__, $member);
414
		if($extended !== null) return $extended;
415
416
		return $this->userHasAccess($member);
417
	}
418
419
	/**
420
	 *
421
	 * @param \Member $member
422
	 * @return boolean
423
	 */
424
	public function canDelete($member = null) {
425
		$extended = $this->extendedCan(__FUNCTION__, $member);
426
		if($extended !== null) return $extended;
427
428
		if(Permission::checkMember($member, "DELETE_WORKFLOW")) {
429
			return true;
430
		}
431
		return false;
432
	}
433
434
	/**
435
	 * Checks whether the given user is in the list of users assigned to this
436
	 * workflow
437
	 *
438
	 * @param $memberID
439
	 */
440
	protected function userHasAccess($member) {
441
		if (!$member) {
442
			if (!Member::currentUserID()) {
443
				return false;
444
			}
445
			$member = Member::currentUser();
446
		}
447
448
		if(Permission::checkMember($member, "ADMIN")) {
449
			return true;
450
		}
451
452
		// This method primarily "protects" access to a WorkflowInstance, but assumes access only to be granted to users assigned-to that WorkflowInstance.
453
		// However; lowly authors (users entering items into a workflow) are not assigned - but we still wish them to see their submitted content.
454
		$inWorkflowGroupOrUserTables = ($member->inGroups($this->Groups()) || $this->Users()->find('ID', $member->ID));
455
		// This method is used in more than just the ModelAdmin. Check for the current controller to determine where canView() expectations differ
456
		if($this->getTarget() && Controller::curr()->getAction() == 'index' && !$inWorkflowGroupOrUserTables) {
457
			if($this->getVersionedConnection($this->getTarget()->ID, $member->ID)) {
458
				return true;
459
			}
460
			return false;
461
		}
462
		return $inWorkflowGroupOrUserTables;
463
	}
464
465
	/**
466
	 * Can documents in the current workflow state be edited?
467
	 */
468
	public function canEditTarget() {
469
		if ($this->CurrentActionID) {
470
			return $this->CurrentAction()->canEditTarget($this->getTarget());
0 ignored issues
show
It seems like $this->getTarget() can be null; however, canEditTarget() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
471
		}
472
	}
473
474
	/**
475
	 * Does this action restrict viewing of the document?
476
	 *
477
	 * @return boolean
478
	 */
479
	public function canViewTarget() {
480
		$action = $this->CurrentAction();
481
		if ($action) {
482
			return $action->canViewTarget($this->getTarget());
483
		}
484
		return true;
485
	}
486
487
	/**
488
	 * Does this action restrict the publishing of a document?
489
	 *
490
	 * @return boolean
491
	 */
492
	public function canPublishTarget() {
493
		if ($this->CurrentActionID) {
494
			return $this->CurrentAction()->canPublishTarget($this->getTarget());
0 ignored issues
show
It seems like $this->getTarget() can be null; however, canPublishTarget() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
495
		}
496
	}
497
498
	/**
499
	 * Get the current set of transitions that are valid for the current workflow state,
500
	 * and are available to the current user.
501
	 *
502
	 * @return array
503
	 */
504
	public function validTransitions() {
505
		$action    = $this->CurrentAction();
506
		$transitions = $action->getValidTransitions();
507
508
		// Filter by execute permission
509
		$self = $this;
510
		return $transitions->filterByCallback(function($transition) use ($self) {
511
			return $transition->canExecute($self);
512
		});
513
	}
514
515
	/* UI RELATED METHODS */
516
517
	/**
518
	 * Gets fields for managing this workflow instance in its current step
519
	 *
520
	 * @return FieldList
521
	 */
522
	public function getWorkflowFields() {
523
		$action    = $this->CurrentAction();
524
		$options   = $this->validTransitions();
525
		$wfOptions = $options->map('ID', 'Title', ' ');
526
		$fields    = new FieldList();
527
528
		$fields->push(new HeaderField('WorkflowHeader', $action->Title));
529
530
		$fields->push(HiddenField::create('TransitionID', ''));
531
		// Let the Active Action update the fields that the user can interact with so that data can be
532
		// stored for the workflow.
533
		$action->updateWorkflowFields($fields);
534
535
		return $fields;
536
	}
537
538
	/**
539
	 * Gets Front-End form fields from current Action
540
	 *
541
	 * @return FieldList
542
	 */
543
	public function getFrontEndWorkflowFields() {
544
		$action = $this->CurrentAction();
545
546
		$fields = new FieldList();
547
		$action->updateFrontEndWorkflowFields($fields);
548
549
		return $fields;
550
	}
551
552
	/**
553
	 * Gets Transitions for display as Front-End Form Actions
554
	 *
555
	 * @return FieldList
556
	 */
557
	public function getFrontEndWorkflowActions() {
558
		$action    = $this->CurrentAction();
559
		$options   = $action->getValidTransitions();
560
		$actions   = new FieldList();
561
562
		$hide_disabled_actions_on_frontend = $this->config()->hide_disabled_actions_on_frontend; 
563
564
		foreach ($options as $option) {
565
			$btn = new FormAction("transition_{$option->ID}", $option->Title);
566
567
			// add cancel class to passive actions, this prevents js validation (using jquery.validate)
568
			if($option->Type == 'Passive'){
569
				$btn->addExtraClass('cancel');
570
			}
571
572
			// disable the button if canExecute() returns false
573
			if(!$option->canExecute($this))
574
			{
575
				if ($hide_disabled_actions_on_frontend)
576
				{
577
					continue;
578
				}
579
580
				$btn = $btn->performReadonlyTransformation();
581
				$btn->addExtraClass('hide');
582
			}
583
584
			$actions->push($btn);
585
		}
586
587
		$action->updateFrontEndWorkflowActions($actions);
588
589
		return $actions;
590
	}
591
592
	/**
593
	 * Gets Front-End DataObject
594
	 *
595
	 * @return DataObject
596
	 */
597
	public function getFrontEndDataObject() {
598
		$action = $this->CurrentAction();
599
		$obj = $action->getFrontEndDataObject();
600
601
		return $obj;
602
	}
603
604
	/**
605
	 * Gets Front-End DataObject
606
	 *
607
	 * @return DataObject
608
	 */
609
	public function getFrontEndRequiredFields() {
610
		$action = $this->CurrentAction();
611
		$validator = $action->getRequiredFields();
612
613
		return $validator;
614
	}
615
616
	public function setFrontendFormRequirements() {
617
		$action = $this->CurrentAction();
618
		$action->setFrontendFormRequirements();
619
	}
620
621
	public function doFrontEndAction(array $data, Form $form, SS_HTTPRequest $request) {
622
		$action = $this->CurrentAction();
623
		$action->doFrontEndAction($data, $form, $request);
624
	}
625
626
	/*
627
	 * 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
628
	 * @see {@link $this->userHasAccess()}
629
	 *
630
	 * @param number $recordID
631
	 * @param number $userID
632
	 * @param number $wasPublished
633
	 * @return boolean
634
	 */
635
	public function getVersionedConnection($recordID, $userID, $wasPublished = 0) {
636
		// Turn this into an array and run through implode()
637
		$filter = "RecordID = {$recordID} AND AuthorID = {$userID} AND WasPublished = {$wasPublished}";
638
		$query = new SQLQuery();
639
		$query->setFrom('"SiteTree_versions"')->setSelect('COUNT("ID")')->setWhere($filter);
640
		$query->firstRow();
641
		$hasAuthored = $query->execute();
642
		if($hasAuthored) {
643
			return true;
644
		}
645
		return false;
646
	}
647
648
	/*
649
	 * Simple method to retrieve the current action, on the current WorkflowInstance
650
	 */
651
	public function getCurrentAction() {
652
		$join = '"WorkflowAction"."ID" = "WorkflowActionInstance"."BaseActionID"';
653
		$action = WorkflowAction::get()
654
					->leftJoin('WorkflowActionInstance',$join)
655
					->where('"WorkflowActionInstance"."ID" = '.$this->CurrentActionID)
656
					->first();
657
		if(!$action) {
658
			return 'N/A';
659
		}
660
		return $action->getField('Title');
661
	}
662
663
	/**
664
	 * Tells us if $member has had permissions over some part of the current WorkflowInstance.
665
	 *
666
	 * @param $member
667
	 * @return \WorkflowAction | boolean
668
	 */
669
	public function getMostRecentActionForUser($member = null) {
670
		if(!$member) {
671
			if (!Member::currentUserID()) {
672
				return false;
673
			}
674
			$member = Member::currentUser();
675
		}
676
677
		// WorkflowActionInstances in reverse creation-order so we get the most recent one's first
678
		$history = $this->Actions()->filter(array(
679
			'Finished' =>1,
680
			'BaseAction.ClassName' => 'AssignUsersToWorkflowAction'
681
		))->Sort('Created', 'DESC');
682
683
		$i=0;
684
		foreach($history as $inst) {
685
			/*
686
			 * This iteration represents the 1st instance in the list - the most recent AssignUsersToWorkflowAction in $history.
687
			 * If there's no match for $member here or on the _previous_ AssignUsersToWorkflowAction, then bail out:
688
			 */
689
			$assignedMembers = $inst->BaseAction()->getAssignedMembers();
690
			if($i<=1 && $assignedMembers->count()>0 && $assignedMembers->find('ID', $member->ID)) {
691
				return $inst;
692
			}
693
			++$i;
694
		}
695
		return false;
696
	}
697
}