From 947c53883133bdb28a5545bc0e3a7dd895efd069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4se?= Date: Wed, 25 Dec 2024 12:42:28 +0100 Subject: [PATCH 1/2] Add lifecycle history field type --- .../Field/FieldType/LifecycleHistoryItem.php | 108 ++++++++++++++++++ .../Plugin/Field/FieldType/LifecycleItem.php | 6 +- .../projects/projects/src/Entity/Project.php | 12 +- .../Field/UserIsManagerFieldItemList.php | 6 +- .../projects/src/ProjectInterface.php | 5 +- 5 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleHistoryItem.php diff --git a/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleHistoryItem.php b/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleHistoryItem.php new file mode 100644 index 00000000..948a7bb1 --- /dev/null +++ b/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleHistoryItem.php @@ -0,0 +1,108 @@ +setLabel(new TranslatableMarkup('Timestamp')) + ->setDescription(new TranslatableMarkup('The time of the transition.')) + ->setRequired(TRUE); + + $properties['type'] = DataDefinition::create('string') + ->setLabel(new TranslatableMarkup('Type')) + ->setDescription(new TranslatableMarkup('The type of the transition.')) + ->setRequired(TRUE); + + $properties['from'] = DataDefinition::create('string') + ->setLabel(new TranslatableMarkup('From')) + ->setDescription(new TranslatableMarkup('The state that the transition started from.')) + ->setRequired(TRUE); + + $properties['to'] = DataDefinition::create('string') + ->setLabel(new TranslatableMarkup('To')) + ->setDescription(new TranslatableMarkup('The state that the transition went to.')) + ->setRequired(TRUE); + + $properties['uid'] = DataDefinition::create('integer') + ->setLabel(new TranslatableMarkup('Initiator')) + ->setDescription(new TranslatableMarkup('The initiator of the transition.')) + ->setSetting('unsigned', TRUE) + ->setRequired(TRUE); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition): array { + return [ + 'columns' => [ + 'timestamp' => [ + 'description' => 'The time of the transition.', + 'type' => 'int', + 'unsigned' => TRUE, + ], + 'type' => [ + 'description' => 'The type of the transition.', + 'type' => 'varchar', + 'length' => 64, + ], + 'from' => [ + 'description' => 'The state that the transition started from.', + 'type' => 'varchar', + 'length' => 64, + ], + 'to' => [ + 'description' => 'The state that the transition went to.', + 'type' => 'varchar', + 'length' => 64, + ], + 'uid' => [ + 'description' => 'The initiator of the transition.', + 'type' => 'int', + 'unsigned' => TRUE, + ], + ], + 'indexes' => [ + 'format' => ['type'], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function isEmpty(): bool { + return FALSE; + } + +} diff --git a/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleItem.php b/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleItem.php index 2364f0ea..4989aa15 100644 --- a/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleItem.php +++ b/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleItem.php @@ -16,12 +16,12 @@ use Drupal\workflows\WorkflowInterface; /** - * Workflow state field item. + * Provides a lifecyle workflow state field item. * * @FieldType( * id = "lifecycle_item", - * label = @Translation("Workflows"), - * description = @Translation("Allows you to store a workflow state."), + * label = @Translation("Lifecycle"), + * description = @Translation("Allows you to store a lifecycle workflow state."), * constraints = {"LifecycleConstraint" = {}}, * default_formatter = "lifecycle_state_list", * default_widget = "options_select" diff --git a/web/modules/custom/projects/projects/src/Entity/Project.php b/web/modules/custom/projects/projects/src/Entity/Project.php index 109f6e0c..9d902aa2 100644 --- a/web/modules/custom/projects/projects/src/Entity/Project.php +++ b/web/modules/custom/projects/projects/src/Entity/Project.php @@ -12,6 +12,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Session\AccountInterface; +use Drupal\creatives\Entity\Creative; use Drupal\organizations\Entity\Organization; use Drupal\projects\Event\ProjectCreateEvent; use Drupal\projects\Plugin\Field\UserIsApplicantFieldItemList; @@ -175,11 +176,14 @@ public function access($operation, ?AccountInterface $account = NULL, $return_as * * Overwritten method for type hinting. */ - public function getOwner(): Organization { + public function getOwner(): Organization|Creative { $key = $this->getEntityType()->getKey('owner'); - /** @var \Drupal\organizations\Entity\Organization $organization */ - $organization = $this->get($key)->entity; - return $organization; + /** @var \Drupal\organizations\Entity\Organization|\Drupal\creatives\Entity\Creative $owner */ + $owner = $this->get($key)->entity; + if (!$owner instanceof Organization && !$owner->hasPermission('administer site')) { + throw new \LogicException('The owner of a project should be an organization'); + } + return $owner; } /** diff --git a/web/modules/custom/projects/projects/src/Plugin/Field/UserIsManagerFieldItemList.php b/web/modules/custom/projects/projects/src/Plugin/Field/UserIsManagerFieldItemList.php index 7644b4a6..82120f3b 100644 --- a/web/modules/custom/projects/projects/src/Plugin/Field/UserIsManagerFieldItemList.php +++ b/web/modules/custom/projects/projects/src/Plugin/Field/UserIsManagerFieldItemList.php @@ -4,6 +4,7 @@ use Drupal\Core\Field\FieldItemList; use Drupal\Core\TypedData\ComputedItemListTrait; +use Drupal\organizations\ManagerInterface; /** * AppliedFieldItemList class to generate a computed field. @@ -29,8 +30,11 @@ protected function computeValue(): void { $account = \Drupal::currentUser(); // Set manager status. + $owner = $project->getOwner(); + $value = $owner instanceof ManagerInterface ? + $project->getOwner()->isManager($account) : FALSE; /** @var \Drupal\youvo\Plugin\Field\FieldType\CacheableBooleanItem $item */ - $item = $this->createItem(0, $project->getOwner()->isManager($account)); + $item = $this->createItem(0, $value); $item->getValueProperty()->mergeCacheMaxAge(0); $this->list[] = $item; } diff --git a/web/modules/custom/projects/projects/src/ProjectInterface.php b/web/modules/custom/projects/projects/src/ProjectInterface.php index 3172a885..5a3df2dd 100644 --- a/web/modules/custom/projects/projects/src/ProjectInterface.php +++ b/web/modules/custom/projects/projects/src/ProjectInterface.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\creatives\Entity\Creative; use Drupal\organizations\Entity\Organization; use Drupal\projects\Service\ProjectLifecycleInterface; use Drupal\user\EntityOwnerInterface; @@ -176,7 +177,9 @@ public function getResult(): ProjectResultInterface; * Returns the entity owner's user entity. * * Overwrite method for type hinting. + * + * Might be a creative user during administrative tasks. */ - public function getOwner(): Organization; + public function getOwner(): Organization|Creative; } From 3541821bbf0561594d0a2477aff7b791f69ae5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4se?= Date: Thu, 26 Dec 2024 23:31:10 +0100 Subject: [PATCH 2/2] Inscribe lifecycle history with every project transition --- ...y_form_display.project.project.default.yml | 7 ++ ...y_view_display.project.project.default.yml | 8 +++ ...roject.project.field_lifecycle_history.yml | 21 ++++++ ...torage.project.field_lifecycle_history.yml | 19 +++++ .../Field/FieldType/LifecycleHistoryItem.php | 40 ++++++----- .../projects/projects/projects.services.yml | 2 +- .../projects/projects/src/Entity/Project.php | 18 +++++ .../projects/src/Service/ProjectLifecycle.php | 30 +++++++- .../src/Service/ProjectLifecycleInterface.php | 6 ++ ...roject.project.field_lifecycle_history.yml | 20 ++++++ ...torage.project.field_lifecycle_history.yml | 18 +++++ .../EventSubscriber/ProjectCompleteTest.php | 5 ++ .../EventSubscriber/ProjectMediateTest.php | 5 ++ .../EventSubscriber/ProjectPublishTest.php | 6 ++ .../EventSubscriber/ProjectResetTest.php | 6 ++ .../EventSubscriber/ProjectSubmitTest.php | 6 ++ .../tests/src/Unit/ProjectLifecycleTest.php | 71 +++++++++++-------- 17 files changed, 239 insertions(+), 49 deletions(-) create mode 100644 config/sync/field.field.project.project.field_lifecycle_history.yml create mode 100644 config/sync/field.storage.project.field_lifecycle_history.yml create mode 100644 web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_lifecycle_history.yml create mode 100644 web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_lifecycle_history.yml diff --git a/config/sync/core.entity_form_display.project.project.default.yml b/config/sync/core.entity_form_display.project.project.default.yml index 5e5d9802..f8fbd19d 100644 --- a/config/sync/core.entity_form_display.project.project.default.yml +++ b/config/sync/core.entity_form_display.project.project.default.yml @@ -13,6 +13,7 @@ dependencies: - field.field.project.project.field_image - field.field.project.project.field_image_copyright - field.field.project.project.field_lifecycle + - field.field.project.project.field_lifecycle_history - field.field.project.project.field_local - field.field.project.project.field_material - field.field.project.project.field_participants @@ -95,6 +96,12 @@ content: region: content settings: { } third_party_settings: { } + field_lifecycle_history: + type: null + weight: 121 + region: content + settings: { } + third_party_settings: { } field_local: type: boolean_checkbox weight: 16 diff --git a/config/sync/core.entity_view_display.project.project.default.yml b/config/sync/core.entity_view_display.project.project.default.yml index fa4b5b50..64e62d08 100644 --- a/config/sync/core.entity_view_display.project.project.default.yml +++ b/config/sync/core.entity_view_display.project.project.default.yml @@ -13,6 +13,7 @@ dependencies: - field.field.project.project.field_image - field.field.project.project.field_image_copyright - field.field.project.project.field_lifecycle + - field.field.project.project.field_lifecycle_history - field.field.project.project.field_local - field.field.project.project.field_material - field.field.project.project.field_participants @@ -96,6 +97,13 @@ content: third_party_settings: { } weight: 8 region: content + field_lifecycle_history: + type: null + label: above + settings: { } + third_party_settings: { } + weight: 18 + region: content field_local: type: boolean label: above diff --git a/config/sync/field.field.project.project.field_lifecycle_history.yml b/config/sync/field.field.project.project.field_lifecycle_history.yml new file mode 100644 index 00000000..2075380c --- /dev/null +++ b/config/sync/field.field.project.project.field_lifecycle_history.yml @@ -0,0 +1,21 @@ +uuid: 4f801fce-81be-465a-9383-d9a3a9f92137 +langcode: de +status: true +dependencies: + config: + - field.storage.project.field_lifecycle_history + module: + - lifecycle + - projects +id: project.project.field_lifecycle_history +field_name: field_lifecycle_history +entity_type: project +bundle: project +label: 'Lifecycle History' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: lifecycle_history_item diff --git a/config/sync/field.storage.project.field_lifecycle_history.yml b/config/sync/field.storage.project.field_lifecycle_history.yml new file mode 100644 index 00000000..7aac0db2 --- /dev/null +++ b/config/sync/field.storage.project.field_lifecycle_history.yml @@ -0,0 +1,19 @@ +uuid: e80ad92f-3570-49a2-b92c-2e23ae1d74e2 +langcode: de +status: true +dependencies: + module: + - lifecycle + - projects +id: project.field_lifecycle_history +field_name: field_lifecycle_history +entity_type: project +type: lifecycle_history_item +settings: { } +module: lifecycle +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleHistoryItem.php b/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleHistoryItem.php index 948a7bb1..4862d20a 100644 --- a/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleHistoryItem.php +++ b/web/modules/custom/projects/lifecycle/src/Plugin/Field/FieldType/LifecycleHistoryItem.php @@ -22,7 +22,11 @@ * default_widget = NULL, * ) * - * @property string|null $value + * @property string|null $transition + * @property string|null $from + * @property string $to + * @property int $uid + * @property int $timestamp */ class LifecycleHistoryItem extends FieldItemBase { @@ -31,20 +35,13 @@ class LifecycleHistoryItem extends FieldItemBase { */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array { - $properties['timestamp'] = DataDefinition::create('timestamp') - ->setLabel(new TranslatableMarkup('Timestamp')) - ->setDescription(new TranslatableMarkup('The time of the transition.')) - ->setRequired(TRUE); - - $properties['type'] = DataDefinition::create('string') - ->setLabel(new TranslatableMarkup('Type')) - ->setDescription(new TranslatableMarkup('The type of the transition.')) - ->setRequired(TRUE); + $properties['transition'] = DataDefinition::create('string') + ->setLabel(new TranslatableMarkup('Transition')) + ->setDescription(new TranslatableMarkup('The type of the transition.')); $properties['from'] = DataDefinition::create('string') ->setLabel(new TranslatableMarkup('From')) - ->setDescription(new TranslatableMarkup('The state that the transition started from.')) - ->setRequired(TRUE); + ->setDescription(new TranslatableMarkup('The state that the transition started from.')); $properties['to'] = DataDefinition::create('string') ->setLabel(new TranslatableMarkup('To')) @@ -57,6 +54,11 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel ->setSetting('unsigned', TRUE) ->setRequired(TRUE); + $properties['timestamp'] = DataDefinition::create('timestamp') + ->setLabel(new TranslatableMarkup('Timestamp')) + ->setDescription(new TranslatableMarkup('The time of the transition.')) + ->setRequired(TRUE); + return $properties; } @@ -66,12 +68,7 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel public static function schema(FieldStorageDefinitionInterface $field_definition): array { return [ 'columns' => [ - 'timestamp' => [ - 'description' => 'The time of the transition.', - 'type' => 'int', - 'unsigned' => TRUE, - ], - 'type' => [ + 'transition' => [ 'description' => 'The type of the transition.', 'type' => 'varchar', 'length' => 64, @@ -91,9 +88,14 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) 'type' => 'int', 'unsigned' => TRUE, ], + 'timestamp' => [ + 'description' => 'The time of the transition.', + 'type' => 'int', + 'unsigned' => TRUE, + ], ], 'indexes' => [ - 'format' => ['type'], + 'format' => ['transition'], ], ]; } diff --git a/web/modules/custom/projects/projects/projects.services.yml b/web/modules/custom/projects/projects/projects.services.yml index 5b1493c6..b1c07dc2 100644 --- a/web/modules/custom/projects/projects/projects.services.yml +++ b/web/modules/custom/projects/projects/projects.services.yml @@ -3,7 +3,7 @@ services: project.lifecycle: class: Drupal\projects\Service\ProjectLifecycle arguments: - [ '@entity_type.manager' ] + [ '@current_user', '@entity_type.manager', '@datetime.time' ] logger.channel.projects: parent: logger.channel_base arguments: [ 'projects' ] diff --git a/web/modules/custom/projects/projects/src/Entity/Project.php b/web/modules/custom/projects/projects/src/Entity/Project.php index 9d902aa2..22ca4431 100644 --- a/web/modules/custom/projects/projects/src/Entity/Project.php +++ b/web/modules/custom/projects/projects/src/Entity/Project.php @@ -20,6 +20,7 @@ use Drupal\projects\Plugin\Field\UserIsParticipantFieldItemList; use Drupal\projects\ProjectInterface; use Drupal\projects\ProjectResultInterface; +use Drupal\projects\ProjectState; use Drupal\projects\Service\ProjectLifecycleInterface; use Drupal\user\EntityOwnerTrait; use Drupal\user_types\Utility\Profile; @@ -114,6 +115,23 @@ public function delete(): void { parent::delete(); } + /** + * {@inheritdoc} + */ + public function postCreate(EntityStorageInterface $storage): void { + // Set first item in lifecycle history. + if ($this->hasField('field_lifecycle_history')) { + $this->set('field_lifecycle_history', [ + 'transition' => NULL, + 'from' => NULL, + 'to' => ProjectState::DRAFT->value, + 'uid' => \Drupal::currentUser()->id(), + 'timestamp' => $this->getCreatedTime(), + ]); + } + parent::postCreate($storage); + } + /** * {@inheritdoc} */ diff --git a/web/modules/custom/projects/projects/src/Service/ProjectLifecycle.php b/web/modules/custom/projects/projects/src/Service/ProjectLifecycle.php index b61a8fb5..63cba0cb 100644 --- a/web/modules/custom/projects/projects/src/Service/ProjectLifecycle.php +++ b/web/modules/custom/projects/projects/src/Service/ProjectLifecycle.php @@ -2,7 +2,10 @@ namespace Drupal\projects\Service; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Session\AccountProxyInterface; use Drupal\lifecycle\Exception\LifecycleTransitionException; use Drupal\projects\ProjectInterface; use Drupal\projects\ProjectState; @@ -15,6 +18,7 @@ class ProjectLifecycle implements ProjectLifecycleInterface { const WORKFLOW_ID = 'project_lifecycle'; const LIFECYCLE_FIELD = 'field_lifecycle'; + const LIFECYCLE_HISTORY_FIELD = 'field_lifecycle_history'; /** * The project calling the lifecycle. @@ -27,7 +31,9 @@ class ProjectLifecycle implements ProjectLifecycleInterface { * Constructs a ProjectLifecycle object. */ public function __construct( + protected AccountProxyInterface $currentUser, protected EntityTypeManagerInterface $entityTypeManager, + protected TimeInterface $time, ) {} /** @@ -126,6 +132,13 @@ public function reset(): bool { return $this->doTransition(ProjectTransition::RESET); } + /** + * {@inheritdoc} + */ + public function history(): FieldItemListInterface { + return $this->project()->get(static::LIFECYCLE_HISTORY_FIELD); + } + /** * Abstraction of forward transition flow check. */ @@ -152,9 +165,11 @@ protected function canTransition(ProjectTransition $transition, ProjectState $fr * Sets new project state for given transition. */ protected function doTransition(ProjectTransition $transition): bool { + $old_state = $this->getState(); $new_state = $this->getSuccessorFromTransition($transition); - if ($this->canTransition($transition, $this->getState(), $new_state)) { + if ($this->canTransition($transition, $old_state, $new_state)) { $this->project()->set(static::LIFECYCLE_FIELD, $new_state->value); + $this->inscribeTransition($transition, $old_state, $new_state); return TRUE; } throw new LifecycleTransitionException($transition->value); @@ -174,4 +189,17 @@ protected function getSuccessorFromTransition(ProjectTransition $transition): Pr }; } + /** + * Inscribes transition in lifecycle history. + */ + protected function inscribeTransition(ProjectTransition $transition, ProjectState $from, ProjectState $to): void { + $this->project()->get(static::LIFECYCLE_HISTORY_FIELD)->appendItem([ + 'transition' => $transition->value, + 'from' => $from->value, + 'to' => $to->value, + 'uid' => $this->currentUser->id(), + 'timestamp' => $this->time->getCurrentTime(), + ]); + } + } diff --git a/web/modules/custom/projects/projects/src/Service/ProjectLifecycleInterface.php b/web/modules/custom/projects/projects/src/Service/ProjectLifecycleInterface.php index c436b9e6..76d590bf 100644 --- a/web/modules/custom/projects/projects/src/Service/ProjectLifecycleInterface.php +++ b/web/modules/custom/projects/projects/src/Service/ProjectLifecycleInterface.php @@ -2,6 +2,7 @@ namespace Drupal\projects\Service; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\projects\ProjectInterface; /** @@ -69,4 +70,9 @@ public function complete(): bool; */ public function reset(): bool; + /** + * Gets the lifecycle history. + */ + public function history(): FieldItemListInterface; + } diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_lifecycle_history.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_lifecycle_history.yml new file mode 100644 index 00000000..4dac8031 --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.field.project.project.field_lifecycle_history.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.project.field_lifecycle_history + module: + - lifecycle + - projects +id: project.project.field_lifecycle_history +field_name: field_lifecycle_history +entity_type: project +bundle: project +label: 'Lifecycle History' +description: '' +required: false +translatable: false +default_value: { } +default_value_callback: '' +settings: { } +field_type: lifecycle_history_item diff --git a/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_lifecycle_history.yml b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_lifecycle_history.yml new file mode 100644 index 00000000..fe99cade --- /dev/null +++ b/web/modules/custom/projects/projects/tests/modules/projects_lifecycle_test/config/install/field.storage.project.field_lifecycle_history.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - lifecycle + - projects +id: project.field_lifecycle_history +field_name: field_lifecycle_history +entity_type: project +type: lifecycle_history_item +settings: { } +module: lifecycle +locked: false +cardinality: -1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectCompleteTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectCompleteTest.php index 71c030d3..e3bd4a72 100644 --- a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectCompleteTest.php +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectCompleteTest.php @@ -4,6 +4,7 @@ use Drupal\projects\Event\ProjectCompleteEvent; use Drupal\projects\ProjectState; +use Drupal\projects\ProjectTransition; /** * Tests for the project complete event subscriber. @@ -48,6 +49,10 @@ public function testProjectComplete(): void { $this->eventDispatcher->dispatch($event); $this->assertTrue($project->lifecycle()->isCompleted()); + + /** @var \Drupal\lifecycle\Plugin\Field\FieldType\LifecycleHistoryItem $last */ + $last = $project->lifecycle()->history()->last(); + $this->assertEquals(ProjectTransition::COMPLETE->value, $last->transition); } } diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectMediateTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectMediateTest.php index e60831ec..50d86919 100644 --- a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectMediateTest.php +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectMediateTest.php @@ -4,6 +4,7 @@ use Drupal\projects\Event\ProjectMediateEvent; use Drupal\projects\ProjectState; +use Drupal\projects\ProjectTransition; /** * Tests for the project mediate event subscriber. @@ -35,6 +36,10 @@ public function testProjectMediate(): void { $this->assertTrue($project->lifecycle()->isOngoing()); $this->assertTrue($project->hasParticipant()); $this->assertTrue($project->isParticipant($creative)); + + /** @var \Drupal\lifecycle\Plugin\Field\FieldType\LifecycleHistoryItem $last */ + $last = $project->lifecycle()->history()->last(); + $this->assertEquals(ProjectTransition::MEDIATE->value, $last->transition); } } diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectPublishTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectPublishTest.php index e92516b1..28473226 100644 --- a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectPublishTest.php +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectPublishTest.php @@ -4,6 +4,7 @@ use Drupal\projects\Event\ProjectPublishEvent; use Drupal\projects\ProjectState; +use Drupal\projects\ProjectTransition; /** * Tests for the project publish event subscriber. @@ -20,11 +21,16 @@ class ProjectPublishTest extends ProjectEventSubscriberTestBase { * @covers ::getSubscribedEvents */ public function testProjectPublish(): void { + $project = $this->createProject(ProjectState::PENDING); $this->assertTrue($project->lifecycle()->isPending()); $event = new ProjectPublishEvent($project); $this->eventDispatcher->dispatch($event); $this->assertTrue($project->lifecycle()->isOpen()); + + /** @var \Drupal\lifecycle\Plugin\Field\FieldType\LifecycleHistoryItem $last */ + $last = $project->lifecycle()->history()->last(); + $this->assertEquals(ProjectTransition::PUBLISH->value, $last->transition); } } diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectResetTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectResetTest.php index 8ad33c89..f5118684 100644 --- a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectResetTest.php +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectResetTest.php @@ -4,6 +4,7 @@ use Drupal\projects\Event\ProjectResetEvent; use Drupal\projects\ProjectState; +use Drupal\projects\ProjectTransition; use Drupal\Tests\projects\Kernel\EventSubscriber\ProjectEventSubscriberTestBase; /** @@ -21,11 +22,16 @@ class ProjectResetTest extends ProjectEventSubscriberTestBase { * @covers ::getSubscribedEvents */ public function testProjectReset(): void { + $project = $this->createProject(ProjectState::OPEN); $this->assertTrue($project->lifecycle()->isOpen()); $event = new ProjectResetEvent($project); $this->eventDispatcher->dispatch($event); $this->assertTrue($project->lifecycle()->isDraft()); + + /** @var \Drupal\lifecycle\Plugin\Field\FieldType\LifecycleHistoryItem $last */ + $last = $project->lifecycle()->history()->last(); + $this->assertEquals(ProjectTransition::RESET->value, $last->transition); } } diff --git a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectSubmitTest.php b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectSubmitTest.php index c31b5102..2bfefbdb 100644 --- a/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectSubmitTest.php +++ b/web/modules/custom/projects/projects/tests/src/Kernel/EventSubscriber/ProjectSubmitTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\projects\Kernel\EventSubscriber; use Drupal\projects\Event\ProjectSubmitEvent; +use Drupal\projects\ProjectTransition; /** * Tests for the project submit event subscriber. @@ -19,11 +20,16 @@ class ProjectSubmitTest extends ProjectEventSubscriberTestBase { * @covers ::getSubscribedEvents */ public function testProjectSubmit(): void { + $project = $this->createProject(); $this->assertTrue($project->lifecycle()->isDraft()); $event = new ProjectSubmitEvent($project); $this->eventDispatcher->dispatch($event); $this->assertTrue($project->lifecycle()->isPending()); + + /** @var \Drupal\lifecycle\Plugin\Field\FieldType\LifecycleHistoryItem $last */ + $last = $project->lifecycle()->history()->last(); + $this->assertEquals(ProjectTransition::SUBMIT->value, $last->transition); } } diff --git a/web/modules/custom/projects/projects/tests/src/Unit/ProjectLifecycleTest.php b/web/modules/custom/projects/projects/tests/src/Unit/ProjectLifecycleTest.php index 7557d342..ad950a72 100644 --- a/web/modules/custom/projects/projects/tests/src/Unit/ProjectLifecycleTest.php +++ b/web/modules/custom/projects/projects/tests/src/Unit/ProjectLifecycleTest.php @@ -4,8 +4,11 @@ namespace Drupal\Tests\projects\Unit; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\Core\Session\AccountProxyInterface; use Drupal\lifecycle\Exception\LifecycleTransitionException; use Drupal\lifecycle\Plugin\WorkflowType\Lifecycle; use Drupal\projects\ProjectInterface; @@ -35,48 +38,35 @@ class ProjectLifecycleTest extends UnitTestCase { */ protected function setUp(): void { parent::setUp(); + $current_user = $this->prophesize(AccountProxyInterface::class); $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $this->lifecycle = new ProjectLifecycle($entity_type_manager->reveal()); + $time = $this->prophesize(TimeInterface::class); + $this->lifecycle = new ProjectLifecycle( + $current_user->reveal(), + $entity_type_manager->reveal(), + $time->reveal(), + ); } /** - * Tests the constructor. + * Tests the project methods. * * @covers ::__construct() - */ - public function testConstructor(): void { - $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $lifecycle = new ProjectLifecycle($entity_type_manager->reveal()); - $this->assertInstanceOf(ProjectLifecycle::class, $lifecycle); - $this->assertObjectHasProperty('entityTypeManager', $lifecycle); - } - - /** - * Tests the setProject method. - * * @covers ::setProject - */ - public function testSetProject(): void { - $project = $this->prophesize(ProjectInterface::class)->reveal(); - $self = $this->lifecycle->setProject($project); - $this->assertInstanceOf(ProjectLifecycle::class, $self); - $this->assertSame($project, $this->lifecycle->project()); - } - - /** - * Tests the project method. - * * @covers ::project */ public function testProject(): void { $project = $this->prophesize(ProjectInterface::class)->reveal(); - $this->lifecycle->setProject($project); + $self = $this->lifecycle->setProject($project); + $this->assertInstanceOf(ProjectLifecycle::class, $self); $this->assertSame($project, $this->lifecycle->project()); // Test exception when project is not set properly. - $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $empty_lifecycle = new ProjectLifecycle($entity_type_manager->reveal()); + $current_user = $this->prophesize(AccountProxyInterface::class)->reveal(); + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class)->reveal(); + $time = $this->prophesize(TimeInterface::class)->reveal(); + $empty_lifecycle = new ProjectLifecycle($current_user, $entity_type_manager, $time); $this->expectException(\UnexpectedValueException::class); $empty_lifecycle->project(); } @@ -143,6 +133,7 @@ protected function prophesizeProject(ProjectState $state): void { * @covers ::canTransition * @covers ::doTransition * @covers ::getSuccessorFromTransition + * @covers ::inscribeTransition * * @dataProvider doTransitionProvider */ @@ -212,6 +203,19 @@ public static function doTransitionProvider(): array { return $cases; } + /** + * Tests the history method. + * + * @covers ::history + */ + public function testHistory(): void { + $project = $this->prophesize(ProjectInterface::class); + $history = $this->prophesize(FieldItemListInterface::class)->reveal(); + $project->get(Argument::any())->willReturn($history); + $this->lifecycle->setProject($project->reveal()); + $this->assertSame($history, $this->lifecycle->history()); + } + /** * Prophesizes a project lifecycle with different workflow conditions. */ @@ -230,13 +234,24 @@ protected function prophesizeWorkflow(array $states, array $has_transition, bool $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); $entity_type_manager->getStorage('workflow')->willReturn($workflow_storage->reveal()); + $history = $this->prophesize(FieldItemListInterface::class); + $history->appendItem(Argument::any())->willReturn(NULL); + $values = array_map(static fn($s) => (object) ['value' => $s->value], $states); $project = $this->prophesize(ProjectInterface::class); $project->get(Argument::is(ProjectLifecycle::LIFECYCLE_FIELD))->willReturn(...$values); + $project->get(Argument::is(ProjectLifecycle::LIFECYCLE_HISTORY_FIELD))->willReturn($history); $project->set(Argument::any(), Argument::any())->willReturn(TRUE); $project->hasParticipant(Argument::is('Creative'))->willReturn($has_participant); - $this->lifecycle = new ProjectLifecycle($entity_type_manager->reveal()); + $current_user = $this->prophesize(AccountProxyInterface::class); + $time = $this->prophesize(TimeInterface::class); + + $this->lifecycle = new ProjectLifecycle( + $current_user->reveal(), + $entity_type_manager->reveal(), + $time->reveal() + ); $this->lifecycle->setProject($project->reveal()); }