From 3ace435af3203488ecc648b8cd500800ce57f9ac Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Sun, 9 Jun 2024 00:21:47 +0200 Subject: [PATCH 01/30] Entity Checklist --- .../schema/Version20240608215345.php | 33 +++++ api/src/Entity/Camp.php | 44 +++++++ api/src/Entity/Checklist.php | 114 ++++++++++++++++++ api/src/Repository/ChecklistRepository.php | 29 +++++ api/src/State/ChecklistCreateProcessor.php | 31 +++++ 5 files changed, 251 insertions(+) create mode 100644 api/migrations/schema/Version20240608215345.php create mode 100644 api/src/Entity/Checklist.php create mode 100644 api/src/Repository/ChecklistRepository.php create mode 100644 api/src/State/ChecklistCreateProcessor.php diff --git a/api/migrations/schema/Version20240608215345.php b/api/migrations/schema/Version20240608215345.php new file mode 100644 index 0000000000..f5addee3d0 --- /dev/null +++ b/api/migrations/schema/Version20240608215345.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE checklist (id VARCHAR(16) NOT NULL, createTime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updateTime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, name TEXT NOT NULL, campId VARCHAR(16) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5C696D2F6D299429 ON checklist (campId)'); + $this->addSql('CREATE INDEX IDX_5C696D2F9D468A55 ON checklist (createTime)'); + $this->addSql('CREATE INDEX IDX_5C696D2F55AA53E2 ON checklist (updateTime)'); + $this->addSql('ALTER TABLE checklist ADD CONSTRAINT FK_5C696D2F6D299429 FOREIGN KEY (campId) REFERENCES camp (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE checklist DROP CONSTRAINT FK_5C696D2F6D299429'); + $this->addSql('DROP TABLE checklist'); + } +} diff --git a/api/src/Entity/Camp.php b/api/src/Entity/Camp.php index eea7559c5e..88a895869c 100644 --- a/api/src/Entity/Camp.php +++ b/api/src/Entity/Camp.php @@ -136,6 +136,15 @@ class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototy #[ORM\OneToMany(targetEntity: MaterialList::class, mappedBy: 'camp', orphanRemoval: true, cascade: ['persist'])] public Collection $materialLists; + /** + * List of all Checklists of this Camp. + * Each Checklist is a List of ChecklistItems. + */ + #[ApiProperty(writable: false, example: '["/checklists/1a2b3c4d"]')] + #[Groups(['read'])] + #[ORM\OneToMany(targetEntity: Checklist::class, mappedBy: 'camp', orphanRemoval: true, cascade: ['persist'])] + public Collection $checklists; + /** * List all CampRootContentNodes of this Camp; * Calculated by the View view_camp_root_content_node. @@ -378,6 +387,7 @@ public function __construct() { $this->progressLabels = new ArrayCollection(); $this->activities = new ArrayCollection(); $this->materialLists = new ArrayCollection(); + $this->checklists = new ArrayCollection(); $this->campRootContentNodes = new ArrayCollection(); } @@ -591,6 +601,32 @@ public function removeMaterialList(MaterialList $materialList): self { return $this; } + /** + * @return Checklist[] + */ + public function getChecklists(): array { + return $this->checklists->getValues(); + } + + public function addChecklist(Checklist $checklist): self { + if (!$this->checklists->contains($checklist)) { + $this->checklists[] = $checklist; + $checklist->camp = $this; + } + + return $this; + } + + public function removeChecklist(Checklist $checklist): self { + if ($this->checklists->removeElement($checklist)) { + if ($checklist->camp === $this) { + $checklist->camp = null; + } + } + + return $this; + } + /** * @param Camp $prototype * @param EntityMap $entityMap @@ -608,6 +644,14 @@ public function copyFromPrototype($prototype, $entityMap): void { $materialList->copyFromPrototype($materialListPrototype, $entityMap); } + // copy Checklists + foreach ($prototype->getChecklists() as $checklistPrototype) { + $checklist = new Checklist(); + $this->addChecklist($checklist); + + $checklist->copyFromPrototype($checklistPrototype, $entityMap); + } + // copy Categories foreach ($prototype->getCategories() as $categoryPrototype) { $category = new Category(); diff --git a/api/src/Entity/Checklist.php b/api/src/Entity/Checklist.php new file mode 100644 index 0000000000..5b839f2ff5 --- /dev/null +++ b/api/src/Entity/Checklist.php @@ -0,0 +1,114 @@ + ['delete']] + ), + new GetCollection( + security: 'is_authenticated()' + ), + new Post( + processor: ChecklistCreateProcessor::class, + denormalizationContext: ['groups' => ['write', 'create']], + securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' + ), + new GetCollection( + name: 'BelongsToCamp_App\Entity\Checklist_get_collection', + uriTemplate: self::CAMP_SUBRESOURCE_URI_TEMPLATE, + uriVariables: [ + 'campId' => new Link( + fromClass: Camp::class, + toProperty: 'camp', + security: 'is_granted("CAMP_COLLABORATOR", camp) or is_granted("CAMP_IS_PROTOTYPE", camp)' + ), + ], + ), + ], + denormalizationContext: ['groups' => ['write']], + normalizationContext: ['groups' => ['read']], + order: ['camp.id', 'name'], +)] +#[ApiFilter(filterClass: SearchFilter::class, properties: ['camp'])] +#[ORM\Entity(repositoryClass: ChecklistRepository::class)] +class Checklist extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface { + public const CAMP_SUBRESOURCE_URI_TEMPLATE = '/camps/{campId}/checklists.{_format}'; + + /** + * The camp this checklist belongs to. + */ + #[ApiProperty(example: '/camps/1a2b3c4d')] + #[Groups(['read', 'create'])] + #[ORM\ManyToOne(targetEntity: Camp::class, inversedBy: 'checklists')] + #[ORM\JoinColumn(nullable: false, onDelete: 'cascade')] + public ?Camp $camp = null; + + /** + * Copy contents from this source checklist. + */ + #[ApiProperty(example: '/checklists/1a2b3c4d')] + #[Groups(['create'])] + public ?Checklist $copyChecklistSource; + + /** + * The human readable name of the checklist. + */ + #[ApiProperty(example: 'PBS Ausbildungsziele')] + #[Groups(['read', 'write'])] + #[InputFilter\Trim] + #[InputFilter\CleanText] + #[Assert\NotBlank] + #[Assert\Length(max: 32)] + #[ORM\Column(type: 'text')] + public ?string $name = null; + + public function __construct() { + parent::__construct(); + } + + public function getCamp(): ?Camp { + return $this->camp; + } + + /** + * @param Checklist $prototype + * @param EntityMap $entityMap + */ + public function copyFromPrototype($prototype, $entityMap): void { + $entityMap->add($prototype, $this); + + $this->name = $prototype->name; + } +} diff --git a/api/src/Repository/ChecklistRepository.php b/api/src/Repository/ChecklistRepository.php new file mode 100644 index 0000000000..4325bb3a5a --- /dev/null +++ b/api/src/Repository/ChecklistRepository.php @@ -0,0 +1,29 @@ +getRootAliases()[0]; + $this->filterByCampCollaboration($queryBuilder, $user, "{$rootAlias}.camp"); + } +} diff --git a/api/src/State/ChecklistCreateProcessor.php b/api/src/State/ChecklistCreateProcessor.php new file mode 100644 index 0000000000..bbac6635f5 --- /dev/null +++ b/api/src/State/ChecklistCreateProcessor.php @@ -0,0 +1,31 @@ + + */ +class ChecklistCreateProcessor extends AbstractPersistProcessor { + public function __construct(ProcessorInterface $decorated) { + parent::__construct($decorated); + } + + /** + * @param Checklist $data + */ + public function onBefore($data, Operation $operation, array $uriVariables = [], array $context = []): Checklist { + if (isset($data->copyChecklistSource)) { + // CopyChecklist Source is set -> copy it's content + $entityMap = new EntityMap(); + $data->copyFromPrototype($data->copyChecklistSource, $entityMap); + } + + return $data; + } +} From 764c4d9437202cb453b7d3da11cad5010194beab Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Mon, 10 Jun 2024 19:48:55 +0200 Subject: [PATCH 02/30] add checklist-fixtures --- api/fixtures/checklists.yml | 16 ++++++++++++++++ api/fixtures/performance_test/checklists.yml | 10 ++++++++++ 2 files changed, 26 insertions(+) create mode 100644 api/fixtures/checklists.yml create mode 100644 api/fixtures/performance_test/checklists.yml diff --git a/api/fixtures/checklists.yml b/api/fixtures/checklists.yml new file mode 100644 index 0000000000..e4515cedbd --- /dev/null +++ b/api/fixtures/checklists.yml @@ -0,0 +1,16 @@ +App\Entity\Checklist: + checklist1: + camp: '@camp1' + name: 'J+S Ausbildungsziele' + checklist2WithNoItems: + camp: '@camp1' + name: 'PBS Ausbildungsziele' + checklist1camp2: + camp: '@camp2' + name: 'J+S Ausbildungsziele' + checklist1campUnrelated: + camp: '@campUnrelated' + name: 'J+S Ausbildungsziele' + checklist1campPrototype: + camp: '@campPrototype' + name: 'J+S Ausbildungsziele' diff --git a/api/fixtures/performance_test/checklists.yml b/api/fixtures/performance_test/checklists.yml new file mode 100644 index 0000000000..43a5cd4d59 --- /dev/null +++ b/api/fixtures/performance_test/checklists.yml @@ -0,0 +1,10 @@ +App\Entity\Checklist: + additional_checklist1_{1..400}: + camp: '@additionalCamp_' + name: 'J+S Ausbildungsziele' + additional_checklist2_{1..400}: + camp: '@additionalCamp_' + name: 'PBS Ausbildungsziele' + additional_checklist_camp1_{1..12}: + camp: '@camp1' + name: From 67b0b2b098523148940b2e7737f7d52348a778f6 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Mon, 10 Jun 2024 20:50:02 +0200 Subject: [PATCH 03/30] fix unittests fix unittest --- .../ListCampCollaborationsTest.php | 2 +- .../ReadCampCollaborationTest.php | 2 +- .../Api/SnapshotTests/ReadItemFixtureMap.php | 1 + ... with data set camp_collaborations__1.json | 36 + ...tchesStructure with data set camps__1.json | 9 + ...Structure with data set checklists__1.json | 74 ++ ... with data set camp_collaborations__1.json | 3 + ...tchesStructure with data set camps__1.json | 3 + ...Structure with data set checklists__1.json | 12 + ...hesStructure with data set periods__1.json | 3 + ...est__testOpenApiSpecMatchesSnapshot__1.yml | 917 ++++++++++++++++-- ...t__testRootEndpointMatchesSnapshot__1.json | 4 + ...manceDidNotChangeForStableEndpoints__1.yml | 16 +- 13 files changed, 977 insertions(+), 105 deletions(-) create mode 100644 api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklists__1.json create mode 100644 api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklists__1.json diff --git a/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php b/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php index 06975241fe..8ce744e353 100644 --- a/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php +++ b/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php @@ -114,6 +114,6 @@ public function testSqlQueryCount() { $client->enableProfiler(); $client->request('GET', '/camp_collaborations'); - $this->assertSqlQueryCount($client, 22); + $this->assertSqlQueryCount($client, 25); } } diff --git a/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php b/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php index a9916e6211..8c9eaf027f 100644 --- a/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php +++ b/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php @@ -126,6 +126,6 @@ public function testSqlQueryCount() { $client->enableProfiler(); $client->request('GET', '/camp_collaborations/'.$campCollaboration->getId()); - $this->assertSqlQueryCount($client, 14); + $this->assertSqlQueryCount($client, 15); } } diff --git a/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php b/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php index 15b893263b..f434e34a8a 100644 --- a/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php +++ b/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php @@ -11,6 +11,7 @@ public static function get(string $collectionEndpoint, array $fixtures): mixed { '/camp_collaborations' => $fixtures['campCollaboration1manager'], '/camps' => $fixtures['camp1'], '/categories' => $fixtures['category1'], + '/checklists' => $fixtures['checklist1'], '/content_node/column_layouts' => $fixtures['columnLayout2'], '/content_node/responsive_layouts' => $fixtures['responsiveLayout1'], '/content_types' => $fixtures['contentTypeSafetyConcept'], diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set camp_collaborations__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set camp_collaborations__1.json index e2ff2cbcd4..e347c3664d 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set camp_collaborations__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set camp_collaborations__1.json @@ -14,6 +14,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -81,6 +84,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -163,6 +169,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -245,6 +254,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -327,6 +339,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -409,6 +424,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -491,6 +509,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -573,6 +594,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -655,6 +679,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -737,6 +764,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -819,6 +849,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -901,6 +934,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set camps__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set camps__1.json index 2fa972bec6..567ab885bd 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set camps__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set camps__1.json @@ -12,6 +12,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -59,6 +62,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, @@ -106,6 +112,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklists__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklists__1.json new file mode 100644 index 0000000000..16ae186f1b --- /dev/null +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklists__1.json @@ -0,0 +1,74 @@ +{ + "_embedded": { + "items": [ + { + "_links": { + "camp": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "name": "escaped_value" + }, + { + "_links": { + "camp": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "name": "escaped_value" + }, + { + "_links": { + "camp": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "name": "escaped_value" + }, + { + "_links": { + "camp": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "name": "escaped_value" + } + ] + }, + "_links": { + "items": [ + { + "href": "escaped_value" + }, + { + "href": "escaped_value" + }, + { + "href": "escaped_value" + }, + { + "href": "escaped_value" + } + ], + "self": { + "href": "escaped_value" + } + }, + "totalItems": "escaped_value" +} diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set camp_collaborations__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set camp_collaborations__1.json index c84f1c605b..5867857e14 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set camp_collaborations__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set camp_collaborations__1.json @@ -11,6 +11,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set camps__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set camps__1.json index 9afc1cba01..d7d5123b20 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set camps__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set camps__1.json @@ -402,6 +402,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklists__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklists__1.json new file mode 100644 index 0000000000..39e7e3d07f --- /dev/null +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklists__1.json @@ -0,0 +1,12 @@ +{ + "_links": { + "camp": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "name": "escaped_value" +} diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set periods__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set periods__1.json index 2075727cda..e8083f1bbd 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set periods__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set periods__1.json @@ -11,6 +11,9 @@ "categories": { "href": "escaped_value" }, + "checklists": { + "href": "escaped_value" + }, "creator": { "href": "escaped_value" }, diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index d1f260203b..52c7add8a6 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -2333,6 +2333,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -2460,6 +2469,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -2526,6 +2536,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -2646,6 +2665,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -2708,6 +2728,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -2835,6 +2864,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -2897,6 +2927,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3024,6 +3063,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -3368,6 +3408,8 @@ components: properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } categories: properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + checklists: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } creator: properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } materialLists: @@ -3380,6 +3422,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - periods - progressLabels @@ -3410,6 +3453,8 @@ components: $ref: '#/components/schemas/CampCollaboration.jsonapi' - $ref: '#/components/schemas/CampCollaboration.jsonapi' + - + $ref: '#/components/schemas/CampCollaboration.jsonapi' readOnly: true type: array periods: @@ -3493,6 +3538,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3620,6 +3674,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -3695,6 +3750,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3815,6 +3879,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -3886,6 +3951,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4013,6 +4087,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -4084,6 +4159,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4211,6 +4295,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -4429,6 +4514,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4556,6 +4650,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -4645,6 +4740,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4765,6 +4869,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -4850,6 +4955,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4977,6 +5091,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -5062,6 +5177,15 @@ components: format: iri-reference readOnly: true type: string + checklists: + description: 'List of all Checklists of this Camp.' + example: '["/checklists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -5189,6 +5313,7 @@ components: - activities - campCollaborations - categories + - checklists - materialLists - name - periods @@ -7487,128 +7612,392 @@ components: - preferredContentTypes - short type: object - ColumnLayout-read: + Checklist-read: deprecated: false - description: '' + description: |- + A Checklist + Tree-Structure with ChecklistItems. properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d + camp: + description: 'The camp this checklist belongs to.' + example: /camps/1a2b3c4d format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string - data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - columns: - - - slot: '1' - width: 12 - items: - type: string - type: - - array - - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm + name: + description: 'The human readable name of the checklist.' + example: 'PBS Ausbildungsziele' maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d + type: string + required: + - camp + - name + type: object + Checklist-write: + deprecated: false + description: |- + A Checklist + Tree-Structure with ChecklistItems. + properties: + name: + description: 'The human readable name of the checklist.' + example: 'PBS Ausbildungsziele' + maxLength: 32 + type: string + required: + - name + type: object + Checklist-write_create: + deprecated: false + description: |- + A Checklist + Tree-Structure with ChecklistItems. + properties: + camp: + description: 'The camp this checklist belongs to.' + example: /camps/1a2b3c4d format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d + type: string + copyChecklistSource: + description: 'Copy contents from this source checklist.' + example: /checklists/1a2b3c4d format: iri-reference - readOnly: true type: - 'null' - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' + name: + description: 'The human readable name of the checklist.' + example: 'PBS Ausbildungsziele' maxLength: 32 - type: - - 'null' - - string + type: string required: - - children - - contentType - - data - - position + - camp + - name type: object - ColumnLayout-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + Checklist.jsonapi: deprecated: false - description: '' + description: |- + A Checklist + Tree-Structure with ChecklistItems. properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + name: + description: 'The human readable name of the checklist.' + example: 'PBS Ausbildungsziele' + maxLength: 32 + type: string + required: + - name + type: object + id: + type: string + relationships: + properties: + camp: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + required: + - camp + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' items: - example: 'https://example.com/' - format: iri-reference - type: string + anyOf: + - + $ref: '#/components/schemas/Checklist.jsonapi' readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d + type: object + Checklist.jsonhal-read: + deprecated: false + description: |- + A Checklist + Tree-Structure with ChecklistItems. + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + camp: + description: 'The camp this checklist belongs to.' + example: /camps/1a2b3c4d format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + name: + description: 'The human readable name of the checklist.' + example: 'PBS Ausbildungsziele' + maxLength: 32 + type: string + required: + - camp + - name + type: object + Checklist.jsonhal-write_create: + deprecated: false + description: |- + A Checklist + Tree-Structure with ChecklistItems. + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + camp: + description: 'The camp this checklist belongs to.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + copyChecklistSource: + description: 'Copy contents from this source checklist.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + name: + description: 'The human readable name of the checklist.' + example: 'PBS Ausbildungsziele' + maxLength: 32 + type: string + required: + - camp + - name + type: object + Checklist.jsonld-read: + deprecated: false + description: |- + A Checklist + Tree-Structure with ChecklistItems. + properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + camp: + description: 'The camp this checklist belongs to.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + name: + description: 'The human readable name of the checklist.' + example: 'PBS Ausbildungsziele' + maxLength: 32 + type: string + required: + - camp + - name + type: object + Checklist.jsonld-write_create: + deprecated: false + description: |- + A Checklist + Tree-Structure with ChecklistItems. + properties: + camp: + description: 'The camp this checklist belongs to.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + copyChecklistSource: + description: 'Copy contents from this source checklist.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + name: + description: 'The human readable name of the checklist.' + example: 'PBS Ausbildungsziele' + maxLength: 32 + type: string + required: + - camp + - name + type: object + ColumnLayout-read: + deprecated: false + description: '' + properties: + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 + items: + type: string + type: + - array + - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - children + - contentType + - data + - position + type: object + ColumnLayout-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + deprecated: false + description: '' + properties: + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept readOnly: true type: string data: @@ -22742,6 +23131,93 @@ paths: summary: 'Retrieves the collection of Category resources.' tags: - Category + '/camps/{campId}/checklists': + get: + deprecated: false + description: 'Retrieves the collection of Checklist resources.' + operationId: BelongsToCamp_App\Entity\Checklist_get_collection + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'Checklist identifier' + explode: false + in: path + name: campId + required: true + schema: + type: string + style: simple + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: true + in: query + name: 'camp[]' + required: false + schema: + items: + type: string + type: array + style: form + responses: + 200: + content: + application/hal+json: + schema: + properties: + _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/Checklist.jsonhal-read' }, type: array } }, type: object }, { type: object }] } + _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } + itemsPerPage: { minimum: 0, type: integer } + totalItems: { minimum: 0, type: integer } + required: + - _embedded + - _links + type: object + application/json: + schema: + items: + $ref: '#/components/schemas/Checklist-read' + type: array + application/ld+json: + schema: + properties: + 'hydra:member': { items: { $ref: '#/components/schemas/Checklist.jsonld-read' }, type: array } + 'hydra:search': { properties: { '@type': { type: string }, 'hydra:mapping': { items: { properties: { '@type': { type: string }, property: { type: ['null', string] }, required: { type: boolean }, variable: { type: string } }, type: object }, type: array }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string } }, type: object } + 'hydra:totalItems': { minimum: 0, type: integer } + 'hydra:view': { example: { '@id': string, 'hydra:first': string, 'hydra:last': string, 'hydra:next': string, 'hydra:previous': string, type: string }, properties: { '@id': { format: iri-reference, type: string }, '@type': { type: string }, 'hydra:first': { format: iri-reference, type: string }, 'hydra:last': { format: iri-reference, type: string }, 'hydra:next': { format: iri-reference, type: string }, 'hydra:previous': { format: iri-reference, type: string } }, type: object } + required: + - 'hydra:member' + type: object + application/vnd.api+json: + schema: + items: + $ref: '#/components/schemas/Checklist.jsonapi' + type: array + text/html: + schema: + items: + $ref: '#/components/schemas/Checklist-read' + type: array + description: 'Checklist collection' + summary: 'Retrieves the collection of Checklist resources.' + tags: + - Checklist '/camps/{id}': delete: deprecated: false @@ -23114,6 +23590,255 @@ paths: summary: 'Updates the Category resource.' tags: - Category + /checklists: + get: + deprecated: false + description: 'Retrieves the collection of Checklist resources.' + operationId: api_checklists_get_collection + parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: true + in: query + name: 'camp[]' + required: false + schema: + items: + type: string + type: array + style: form + responses: + 200: + content: + application/hal+json: + schema: + properties: + _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/Checklist.jsonhal-read' }, type: array } }, type: object }, { type: object }] } + _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } + itemsPerPage: { minimum: 0, type: integer } + totalItems: { minimum: 0, type: integer } + required: + - _embedded + - _links + type: object + application/json: + schema: + items: + $ref: '#/components/schemas/Checklist-read' + type: array + application/ld+json: + schema: + properties: + 'hydra:member': { items: { $ref: '#/components/schemas/Checklist.jsonld-read' }, type: array } + 'hydra:search': { properties: { '@type': { type: string }, 'hydra:mapping': { items: { properties: { '@type': { type: string }, property: { type: ['null', string] }, required: { type: boolean }, variable: { type: string } }, type: object }, type: array }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string } }, type: object } + 'hydra:totalItems': { minimum: 0, type: integer } + 'hydra:view': { example: { '@id': string, 'hydra:first': string, 'hydra:last': string, 'hydra:next': string, 'hydra:previous': string, type: string }, properties: { '@id': { format: iri-reference, type: string }, '@type': { type: string }, 'hydra:first': { format: iri-reference, type: string }, 'hydra:last': { format: iri-reference, type: string }, 'hydra:next': { format: iri-reference, type: string }, 'hydra:previous': { format: iri-reference, type: string } }, type: object } + required: + - 'hydra:member' + type: object + application/vnd.api+json: + schema: + items: + $ref: '#/components/schemas/Checklist.jsonapi' + type: array + text/html: + schema: + items: + $ref: '#/components/schemas/Checklist-read' + type: array + description: 'Checklist collection' + summary: 'Retrieves the collection of Checklist resources.' + tags: + - Checklist + post: + deprecated: false + description: 'Creates a Checklist resource.' + operationId: api_checklists_post + parameters: [] + requestBody: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/Checklist.jsonhal-write_create' + application/json: + schema: + $ref: '#/components/schemas/Checklist-write_create' + application/ld+json: + schema: + $ref: '#/components/schemas/Checklist.jsonld-write_create' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Checklist.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/Checklist-write_create' + description: 'The new Checklist resource' + required: true + responses: + 201: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/Checklist.jsonhal-read' + application/json: + schema: + $ref: '#/components/schemas/Checklist-read' + application/ld+json: + schema: + $ref: '#/components/schemas/Checklist.jsonld-read' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Checklist.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/Checklist-read' + description: 'Checklist resource created' + links: [] + 400: + description: 'Invalid input' + 422: + description: 'Unprocessable entity' + summary: 'Creates a Checklist resource.' + tags: + - Checklist + '/checklists/{id}': + delete: + deprecated: false + description: 'Removes the Checklist resource.' + operationId: api_checklists_id_delete + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'Checklist identifier' + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + 204: + description: 'Checklist resource deleted' + 404: + description: 'Resource not found' + summary: 'Removes the Checklist resource.' + tags: + - Checklist + get: + deprecated: false + description: 'Retrieves a Checklist resource.' + operationId: api_checklists_id_get + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'Checklist identifier' + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + 200: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/Checklist.jsonhal-read' + application/json: + schema: + $ref: '#/components/schemas/Checklist-read' + application/ld+json: + schema: + $ref: '#/components/schemas/Checklist.jsonld-read' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Checklist.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/Checklist-read' + description: 'Checklist resource' + 404: + description: 'Resource not found' + summary: 'Retrieves a Checklist resource.' + tags: + - Checklist + patch: + deprecated: false + description: 'Updates the Checklist resource.' + operationId: api_checklists_id_patch + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'Checklist identifier' + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/Checklist-write' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Checklist.jsonapi' + description: 'The updated Checklist resource' + required: true + responses: + 200: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/Checklist.jsonhal-read' + application/json: + schema: + $ref: '#/components/schemas/Checklist-read' + application/ld+json: + schema: + $ref: '#/components/schemas/Checklist.jsonld-read' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Checklist.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/Checklist-read' + description: 'Checklist resource updated' + links: [] + 400: + description: 'Invalid input' + 404: + description: 'Resource not found' + 422: + description: 'Unprocessable entity' + summary: 'Updates the Checklist resource.' + tags: + - Checklist /content_node/column_layouts: get: deprecated: false diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json index 69c3aebd5f..84921e20f9 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json @@ -24,6 +24,10 @@ "href": "\/categories{\/id}{?camp,camp[]}", "templated": true }, + "checklists": { + "href": "\/checklists{\/id}{?camp,camp[]}", + "templated": true + }, "columnLayouts": { "href": "\/content_node\/column_layouts{\/id}{?contentType,contentType[],root,root[],period}", "templated": true diff --git a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml index 1e0924898b..71b7a0fd66 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml @@ -4,12 +4,14 @@ /activity_progress_labels/item: 7 /activity_responsibles: 6 /activity_responsibles/item: 8 -/camps: 26 -/camps/item: 21 -/camp_collaborations: 22 -/camp_collaborations/item: 14 +/camps: 29 +/camps/item: 22 +/camp_collaborations: 25 +/camp_collaborations/item: 15 /categories: 11 /categories/item: 9 +/checklists: 6 +/checklists/item: 7 /content_types: 6 /content_types/item: 6 /days: 26 @@ -21,7 +23,7 @@ /material_lists: 6 /material_lists/item: 7 /periods: 6 -/periods/item: 17 +/periods/item: 18 /profiles: 6 /profiles/item: 6 /schedule_entries: 23 @@ -30,8 +32,8 @@ '/activities?camp=': 13 '/activity_progress_labels?camp=': 6 '/activity_responsibles?activity.camp=': 6 -'/camp_collaborations?camp=': 12 -'/camp_collaborations?activityResponsibles.activity=': 14 +'/camp_collaborations?camp=': 13 +'/camp_collaborations?activityResponsibles.activity=': 15 '/categories?camp=': 9 '/content_types?categories=': 6 '/day_responsibles?day.period=': 6 From 988f96c17c4bc7472066c9e94b151a1561c0c1a2 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Sat, 15 Jun 2024 17:49:12 +0200 Subject: [PATCH 04/30] add Unittests --- .../Api/Checklists/CreateChecklistTest.php | 311 ++++++++++++++++++ .../Api/Checklists/DeleteChecklistTest.php | 87 +++++ .../Api/Checklists/ListChecklistTest.php | 125 +++++++ .../Api/Checklists/ReadChecklistTest.php | 108 ++++++ .../Api/Checklists/UpdateChecklistTest.php | 220 +++++++++++++ 5 files changed, 851 insertions(+) create mode 100644 api/tests/Api/Checklists/CreateChecklistTest.php create mode 100644 api/tests/Api/Checklists/DeleteChecklistTest.php create mode 100644 api/tests/Api/Checklists/ListChecklistTest.php create mode 100644 api/tests/Api/Checklists/ReadChecklistTest.php create mode 100644 api/tests/Api/Checklists/UpdateChecklistTest.php diff --git a/api/tests/Api/Checklists/CreateChecklistTest.php b/api/tests/Api/Checklists/CreateChecklistTest.php new file mode 100644 index 0000000000..12392aca9d --- /dev/null +++ b/api/tests/Api/Checklists/CreateChecklistTest.php @@ -0,0 +1,311 @@ +request('POST', '/checklists', ['json' => $this->getExampleWritePayload()]); + + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testCreateChecklistIsNotPossibleForUnrelatedUserBecauseCampIsNotReadable() { + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('POST', '/checklists', ['json' => $this->getExampleWritePayload()]) + ; + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Item not found for "'.$this->getIriFor('camp1').'".', + ]); + } + + public function testCreateChecklistIsNotPossibleForInactiveCollaboratorBecauseCampIsNotReadable() { + static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('POST', '/checklists', ['json' => $this->getExampleWritePayload()]) + ; + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Item not found for "'.$this->getIriFor('camp1').'".', + ]); + } + + public function testCreateChecklistIsDeniedForGuest() { + static::createClientWithCredentials(['email' => static::$fixtures['user3guest']->getEmail()]) + ->request('POST', '/checklists', ['json' => $this->getExampleWritePayload()]) + ; + + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testCreateChecklistIsAllowedForMember() { + static::createClientWithCredentials(['email' => static::$fixtures['user2member']->getEmail()]) + ->request('POST', '/checklists', ['json' => $this->getExampleWritePayload()]) + ; + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains($this->getExampleReadPayload()); + } + + public function testCreateChecklistIsAllowedForManager() { + static::createClientWithCredentials()->request('POST', '/checklists', ['json' => $this->getExampleWritePayload()]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains($this->getExampleReadPayload()); + } + + public function testCreateChecklistInCampPrototypeIsDeniedForUnrelatedUser() { + static::createClientWithCredentials()->request('POST', '/checklists', ['json' => $this->getExampleWritePayload([ + 'camp' => $this->getIriFor('campPrototype'), + ])]); + + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testCreateChecklistValidatesMissingCamp() { + static::createClientWithCredentials()->request('POST', '/checklists', ['json' => $this->getExampleWritePayload([], ['camp'])]); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'camp', + 'message' => 'This value should not be null.', + ], + ], + ]); + } + + public function testCreateChecklistValidatesMissingName() { + static::createClientWithCredentials()->request('POST', '/checklists', ['json' => $this->getExampleWritePayload([], ['name'])]); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'name', + 'message' => 'This value should not be blank.', + ], + ], + ]); + } + + public function testCreateChecklistValidatesBlankName() { + static::createClientWithCredentials()->request( + 'POST', + '/checklists', + [ + 'json' => $this->getExampleWritePayload( + [ + 'name' => '', + ] + ), + ] + ); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'name', + 'message' => 'This value should not be blank.', + ], + ], + ]); + } + + public function testCreateChecklistValidatesTooLongName() { + static::createClientWithCredentials()->request( + 'POST', + '/checklists', + [ + 'json' => $this->getExampleWritePayload( + [ + 'name' => str_repeat('l', 33), + ] + ), + ] + ); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'name', + 'message' => 'This value is too long. It should have 32 characters or less.', + ], + ], + ]); + } + + public function testCreateChecklistTrimsName() { + static::createClientWithCredentials()->request( + 'POST', + '/checklists', + [ + 'json' => $this->getExampleWritePayload( + [ + 'name' => " \t Ausbildungsziele\t ", + ] + ), + ] + ); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains($this->getExampleReadPayload( + [ + 'name' => 'Ausbildungsziele', + ] + )); + } + + public function testCreateChecklistCleansForbiddenCharactersFromName() { + static::createClientWithCredentials()->request( + 'POST', + '/checklists', + [ + 'json' => $this->getExampleWritePayload( + [ + 'name' => "\n\tAusbildungsziele", + ] + ), + ] + ); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains($this->getExampleReadPayload( + [ + 'name' => 'Ausbildungsziele', + ] + )); + } + + public function testCreateChecklistFromCopySourceValidatesAccess() { + static::createClientWithCredentials(['email' => static::$fixtures['user8memberOnlyInCamp2']->getEmail()])->request( + 'POST', + '/checklists', + ['json' => $this->getExampleWritePayload( + [ + 'camp' => $this->getIriFor('camp2'), + 'copyChecklistSource' => $this->getIriFor('checklist1'), + ] + )] + ); + + // No Access on checklist1 -> BadRequest + $this->assertResponseStatusCodeSame(400); + } + + public function testCreateChecklistFromCopySourceWithinSameCamp() { + static::createClientWithCredentials()->request( + 'POST', + '/checklists', + ['json' => $this->getExampleWritePayload( + [ + 'camp' => $this->getIriFor('camp1'), + 'copyChecklistSource' => $this->getIriFor('checklist1'), + ], + )] + ); + + // Checklist created + $this->assertResponseStatusCodeSame(201); + } + + public function testCreateChecklistFromCopySourceAcrossCamp() { + static::createClientWithCredentials()->request( + 'POST', + '/checklists', + ['json' => $this->getExampleWritePayload( + [ + 'camp' => $this->getIriFor('camp2'), + 'copyChecklistSource' => $this->getIriFor('checklist1'), + ], + )] + ); + + // Checklist created + $this->assertResponseStatusCodeSame(201); + } + + /** + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + */ + public function testCreateResponseStructureMatchesReadResponseStructure() { + $client = static::createClientWithCredentials(); + $client->disableReboot(); + $createResponse = $client->request( + 'POST', + '/checklists', + [ + 'json' => $this->getExampleWritePayload(), + ] + ); + + $this->assertResponseStatusCodeSame(201); + + $createArray = $createResponse->toArray(); + $newItemLink = $createArray['_links']['self']['href']; + $getItemResponse = $client->request('GET', $newItemLink); + + assertThat($createArray, CompatibleHalResponse::isHalCompatibleWith($getItemResponse->toArray())); + } + + public function getExampleWritePayload($attributes = [], $except = []) { + return $this->getExamplePayload( + Checklist::class, + Post::class, + array_merge([ + 'copyChecklistSource' => null, + 'camp' => $this->getIriFor('camp1'), + ], $attributes), + [], + $except + ); + } + + public function getExampleReadPayload($attributes = [], $except = []) { + return $this->getExamplePayload( + Checklist::class, + Get::class, + $attributes, + ['camp', 'preferredContentTypes'], + $except + ); + } +} diff --git a/api/tests/Api/Checklists/DeleteChecklistTest.php b/api/tests/Api/Checklists/DeleteChecklistTest.php new file mode 100644 index 0000000000..f2033769e3 --- /dev/null +++ b/api/tests/Api/Checklists/DeleteChecklistTest.php @@ -0,0 +1,87 @@ +request('DELETE', '/checklists/'.$checklist->getId()); + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testDeleteChecklistIsDeniedForUnrelatedUser() { + $Checklist = static::getFixture('checklist2WithNoItems'); + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('DELETE', '/checklists/'.$Checklist->getId()) + ; + + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testDeleteChecklistIsDeniedForInactiveCollaborator() { + $Checklist = static::getFixture('checklist2WithNoItems'); + static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('DELETE', '/checklists/'.$Checklist->getId()) + ; + + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testDeleteChecklistIsDeniedForGuest() { + $Checklist = static::getFixture('checklist2WithNoItems'); + static::createClientWithCredentials(['email' => static::$fixtures['user3guest']->getEmail()]) + ->request('DELETE', '/checklists/'.$Checklist->getId()) + ; + + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testDeleteChecklistIsAllowedForMember() { + $Checklist = static::getFixture('checklist2WithNoItems'); + static::createClientWithCredentials(['email' => static::$fixtures['user2member']->getEmail()]) + ->request('DELETE', '/checklists/'.$Checklist->getId()) + ; + $this->assertResponseStatusCodeSame(204); + $this->assertNull($this->getEntityManager()->getRepository(Checklist::class)->find($Checklist->getId())); + } + + public function testDeleteChecklistIsAllowedForManager() { + $Checklist = static::getFixture('checklist2WithNoItems'); + static::createClientWithCredentials()->request('DELETE', '/checklists/'.$Checklist->getId()); + $this->assertResponseStatusCodeSame(204); + $this->assertNull($this->getEntityManager()->getRepository(Checklist::class)->find($Checklist->getId())); + } + + public function testDeleteChecklistFromCampPrototypeIsDeniedForUnrelatedUser() { + $Checklist = static::getFixture('checklist1campPrototype'); + static::createClientWithCredentials()->request('DELETE', '/checklists/'.$Checklist->getId()); + + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } +} diff --git a/api/tests/Api/Checklists/ListChecklistTest.php b/api/tests/Api/Checklists/ListChecklistTest.php new file mode 100644 index 0000000000..ef75a68b01 --- /dev/null +++ b/api/tests/Api/Checklists/ListChecklistTest.php @@ -0,0 +1,125 @@ +request('GET', '/checklists'); + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testListChecklistsIsAllowedForLoggedInUserButFiltered() { + // precondition: There is a checklist that the user doesn't have access to + $this->assertNotEmpty(static::$fixtures['checklist1campUnrelated']); + + $response = static::createClientWithCredentials()->request('GET', '/checklists'); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'totalItems' => 4, + '_links' => [ + 'items' => [], + ], + '_embedded' => [ + 'items' => [], + ], + ]); + $this->assertEqualsCanonicalizing([ + ['href' => $this->getIriFor('checklist1')], + ['href' => $this->getIriFor('checklist2WithNoItems')], + ['href' => $this->getIriFor('checklist1camp2')], + ['href' => $this->getIriFor('checklist1campPrototype')], + ], $response->toArray()['_links']['items']); + } + + public function testListChecklistsFilteredByCampIsAllowedForCollaborator() { + $camp = static::getFixture('camp1'); + $response = static::createClientWithCredentials()->request('GET', '/checklists?camp=%2Fcamps%2F'.$camp->getId()); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'totalItems' => 2, + '_links' => [ + 'items' => [], + ], + '_embedded' => [ + 'items' => [], + ], + ]); + $this->assertEqualsCanonicalizing([ + ['href' => $this->getIriFor('checklist1')], + ['href' => $this->getIriFor('checklist2WithNoItems')], + ], $response->toArray()['_links']['items']); + } + + public function testListChecklistsFilteredByCampIsDeniedForUnrelatedUser() { + $camp = static::getFixture('camp1'); + $response = static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('GET', '/checklists?camp=%2Fcamps%2F'.$camp->getId()) + ; + + $this->assertResponseStatusCodeSame(200); + + $this->assertJsonContains(['totalItems' => 0]); + $this->assertArrayNotHasKey('items', $response->toArray()['_links']); + } + + public function testListChecklistsFilteredByCampIsDeniedForInactiveCollaborator() { + $camp = static::getFixture('camp1'); + $response = static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('GET', '/checklists?camp=%2Fcamps%2F'.$camp->getId()) + ; + + $this->assertResponseStatusCodeSame(200); + + $this->assertJsonContains(['totalItems' => 0]); + $this->assertArrayNotHasKey('items', $response->toArray()['_links']); + } + + public function testListChecklistsFilteredByCampPrototypeIsAllowedForUnrelatedUser() { + $camp = static::getFixture('campPrototype'); + $response = static::createClientWithCredentials()->request('GET', '/checklists?camp=%2Fcamps%2F'.$camp->getId()); + + $this->assertResponseStatusCodeSame(200); + + $this->assertJsonContains(['totalItems' => 1]); + $this->assertEqualsCanonicalizing([ + ['href' => $this->getIriFor('checklist1campPrototype')], + ], $response->toArray()['_links']['items']); + } + + public function testListChecklistsAsCampSubresourceIsAllowedForCollaborator() { + $camp = static::getFixture('camp1'); + $response = static::createClientWithCredentials()->request('GET', '/camps/'.$camp->getId().'/checklists'); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'totalItems' => 2, + '_links' => [ + 'items' => [], + ], + '_embedded' => [ + 'items' => [], + ], + ]); + $this->assertEqualsCanonicalizing([ + ['href' => $this->getIriFor('checklist1')], + ['href' => $this->getIriFor('checklist2WithNoItems')], + ], $response->toArray()['_links']['items']); + } + + public function testListChecklistsAsCampSubresourceIsDeniedForUnrelatedUser() { + $camp = static::getFixture('camp1'); + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('GET', '/camps/'.$camp->getId().'/checklists') + ; + + $this->assertResponseStatusCodeSame(404); + } +} diff --git a/api/tests/Api/Checklists/ReadChecklistTest.php b/api/tests/Api/Checklists/ReadChecklistTest.php new file mode 100644 index 0000000000..1857746c1c --- /dev/null +++ b/api/tests/Api/Checklists/ReadChecklistTest.php @@ -0,0 +1,108 @@ +request('GET', '/checklists/'.$checklist->getId()); + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testGetSingleChecklistIsDeniedForUnrelatedUser() { + /** @var Checklist $checklist */ + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('GET', '/checklists/'.$checklist->getId()) + ; + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testGetSingleChecklistIsDeniedForInactiveCollaborator() { + /** @var Checklist $checklist */ + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('GET', '/checklists/'.$checklist->getId()) + ; + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testGetSingleChecklistIsAllowedForGuest() { + /** @var Checklist $checklist */ + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials(['email' => static::$fixtures['user3guest']->getEmail()]) + ->request('GET', '/checklists/'.$checklist->getId()) + ; + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'id' => $checklist->getId(), + 'name' => $checklist->name, + '_links' => [ + 'camp' => ['href' => $this->getIriFor('camp1')], + ], + ]); + } + + public function testGetSingleChecklistIsAllowedForMember() { + /** @var Checklist $checklist */ + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials(['email' => static::$fixtures['user2member']->getEmail()]) + ->request('GET', '/checklists/'.$checklist->getId()) + ; + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'id' => $checklist->getId(), + 'name' => $checklist->name, + '_links' => [ + 'camp' => ['href' => $this->getIriFor('camp1')], + ], + ]); + } + + public function testGetSingleChecklistIsAllowedForManager() { + /** @var Checklist $checklist */ + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials()->request('GET', '/checklists/'.$checklist->getId()); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'id' => $checklist->getId(), + 'name' => $checklist->name, + '_links' => [ + 'camp' => ['href' => $this->getIriFor('camp1')], + ], + ]); + } + + public function testGetSingleChecklistFromCampPrototypeIsAllowedForUnrelatedUser() { + /** @var Checklist $checklist */ + $checklist = static::getFixture('checklist1campPrototype'); + static::createClientWithCredentials()->request('GET', '/checklists/'.$checklist->getId()); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'id' => $checklist->getId(), + 'name' => $checklist->name, + '_links' => [ + 'camp' => ['href' => $this->getIriFor('campPrototype')], + ], + ]); + } +} diff --git a/api/tests/Api/Checklists/UpdateChecklistTest.php b/api/tests/Api/Checklists/UpdateChecklistTest.php new file mode 100644 index 0000000000..3006e3a38e --- /dev/null +++ b/api/tests/Api/Checklists/UpdateChecklistTest.php @@ -0,0 +1,220 @@ +request('PATCH', '/checklists/'.$checklist->getId(), ['json' => [ + 'name' => 'ChecklistName', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testPatchChecklistIsDeniedForUnrelatedUser() { + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('PATCH', '/checklists/'.$checklist->getId(), ['json' => [ + 'name' => 'ChecklistName', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testPatchChecklistIsDeniedForInactiveCollaborator() { + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('PATCH', '/checklists/'.$checklist->getId(), ['json' => [ + 'name' => 'ChecklistName', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testPatchChecklistIsDeniedForGuest() { + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials(['email' => static::$fixtures['user3guest']->getEmail()]) + ->request('PATCH', '/checklists/'.$checklist->getId(), ['json' => [ + 'name' => 'ChecklistName', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testPatchChecklistIsAllowedForMember() { + $checklist = static::getFixture('checklist1'); + $response = static::createClientWithCredentials(['email' => static::$fixtures['user2member']->getEmail()]) + ->request('PATCH', '/checklists/'.$checklist->getId(), ['json' => [ + 'name' => 'ChecklistName', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'name' => 'ChecklistName', + ]); + } + + public function testPatchChecklistIsAllowedForManager() { + $checklist = static::getFixture('checklist1'); + $response = static::createClientWithCredentials()->request('PATCH', '/checklists/'.$checklist->getId(), ['json' => [ + 'name' => 'ChecklistName', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'name' => 'ChecklistName', + ]); + } + + public function testPatchChecklistInCampPrototypeIsDeniedForUnrelatedUser() { + $checklist = static::getFixture('checklist1campPrototype'); + $response = static::createClientWithCredentials()->request('PATCH', '/checklists/'.$checklist->getId(), ['json' => [ + 'name' => 'ChecklistName', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testPatchChecklistDisallowsChangingCamp() { + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials()->request('PATCH', '/checklists/'.$checklist->getId(), ['json' => [ + 'camp' => $this->getIriFor('camp2'), + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains([ + 'detail' => 'Extra attributes are not allowed ("camp" is unknown).', + ]); + } + + public function testPatchChecklistValidatesNullName() { + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials()->request( + 'PATCH', + '/checklists/'.$checklist->getId(), + [ + 'json' => [ + 'name' => null, + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ] + ); + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'The type of the "name" attribute must be "string", "NULL" given.', + ]); + } + + public function testPatchChecklistValidatesBlankName() { + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials()->request( + 'PATCH', + '/checklists/'.$checklist->getId(), + [ + 'json' => [ + 'name' => ' ', + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ] + ); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'name', + 'message' => 'This value should not be blank.', + ], + ], + ]); + } + + public function testPatchChecklistValidatesTooLongName() { + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials()->request( + 'PATCH', + '/checklists/'.$checklist->getId(), + [ + 'json' => [ + 'name' => str_repeat('l', 33), + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ] + ); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'name', + 'message' => 'This value is too long. It should have 32 characters or less.', + ], + ], + ]); + } + + public function testPatchChecklistTrimsName() { + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials()->request( + 'PATCH', + '/checklists/'.$checklist->getId(), + [ + 'json' => [ + 'name' => " \t ChecklistName\t ", + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], ] + ); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains( + [ + 'name' => 'ChecklistName', + ] + ); + } + + public function testPatchChecklistCleansForbiddenCharactersFromName() { + $checklist = static::getFixture('checklist1'); + $client = static::createClientWithCredentials(); + $client->disableReboot(); + $client->request( + 'PATCH', + '/checklists/'.$checklist->getId(), + [ + 'json' => [ + 'name' => "ChecklistName\n\t", + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], ] + ); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains( + [ + 'name' => 'ChecklistName', + ] + ); + } +} From 27a811819d12eae35af64b969208c6101e22fbe3 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Sat, 15 Jun 2024 20:34:05 +0200 Subject: [PATCH 05/30] Add ChecklistItem --- api/fixtures/checklistItems.yml | 23 + .../performance_test/checklistItems.yml | 10 + .../schema/Version20240615180024.php | 36 + api/src/Entity/Checklist.php | 51 + api/src/Entity/ChecklistItem.php | 181 +++ .../Repository/ChecklistItemRepository.php | 37 + .../AssertBelongsToChecklist.php | 10 + .../AssertBelongsToChecklistValidator.php | 39 + .../Validator/ChecklistItem/AssertNoLoop.php | 10 + .../ChecklistItem/AssertNoLoopValidator.php | 46 + .../Api/SnapshotTests/ReadItemFixtureMap.php | 1 + ...ture with data set checklist_items__1.json | 110 ++ ...Structure with data set checklists__1.json | 12 + ...ture with data set checklist_items__1.json | 15 + ...Structure with data set checklists__1.json | 3 + ...est__testOpenApiSpecMatchesSnapshot__1.yml | 1024 +++++++++++++++-- ...t__testRootEndpointMatchesSnapshot__1.json | 4 + ...manceDidNotChangeForStableEndpoints__1.yml | 2 + 18 files changed, 1489 insertions(+), 125 deletions(-) create mode 100644 api/fixtures/checklistItems.yml create mode 100644 api/fixtures/performance_test/checklistItems.yml create mode 100644 api/migrations/schema/Version20240615180024.php create mode 100644 api/src/Entity/ChecklistItem.php create mode 100644 api/src/Repository/ChecklistItemRepository.php create mode 100644 api/src/Validator/ChecklistItem/AssertBelongsToChecklist.php create mode 100644 api/src/Validator/ChecklistItem/AssertBelongsToChecklistValidator.php create mode 100644 api/src/Validator/ChecklistItem/AssertNoLoop.php create mode 100644 api/src/Validator/ChecklistItem/AssertNoLoopValidator.php create mode 100644 api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklist_items__1.json create mode 100644 api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklist_items__1.json diff --git a/api/fixtures/checklistItems.yml b/api/fixtures/checklistItems.yml new file mode 100644 index 0000000000..3892d00a43 --- /dev/null +++ b/api/fixtures/checklistItems.yml @@ -0,0 +1,23 @@ +App\Entity\ChecklistItem: + checklistItem1_1_1: + checklist: '@checklist1' + text: 'Camp1_List1_Item1' + checklistItem1_1_2: + checklist: '@checklist1' + text: 'Camp1_List1_Item2' + checklistItem1_1_2_3: + checklist: '@checklist1' + parent: '@checklistItem1_1_2' + text: 'Camp1_List1_Item2_Item3' +# checklist2WithNoItems: +# checklist: '@checklist2WithNoItems' +# text: 'Camp1_List2_Item1' + checklistItem2_1_1: + checklist: '@checklist1camp2' + text: 'Camp2_List1_Item1' + checklistItemUnrelated_1_1: + checklist: '@checklist1campUnrelated' + text: 'CampUnrelated_List1_Item1' + checklistItemPrototype_1_1: + checklist: '@checklist1campPrototype' + text: 'CampPrototype_List1_Item1' diff --git a/api/fixtures/performance_test/checklistItems.yml b/api/fixtures/performance_test/checklistItems.yml new file mode 100644 index 0000000000..ea01fe011d --- /dev/null +++ b/api/fixtures/performance_test/checklistItems.yml @@ -0,0 +1,10 @@ +App\Entity\ChecklistItem: + additional_checklistItem1_{1..400}: + checklist: '@additional_checklist1_' + text: 'Item_' + additional_checklistItem2_{1..400}: + checklist: '@additional_checklist2_' + text: 'Item_' + additional_checklistItem_camp1_{1..12}: + camp: '@additional_checklist_camp1_1' + text: 'Item_' diff --git a/api/migrations/schema/Version20240615180024.php b/api/migrations/schema/Version20240615180024.php new file mode 100644 index 0000000000..0b3570cb64 --- /dev/null +++ b/api/migrations/schema/Version20240615180024.php @@ -0,0 +1,36 @@ +addSql('CREATE TABLE checklist_item (id VARCHAR(16) NOT NULL, createTime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updateTime TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, text TEXT NOT NULL, position INT NOT NULL, checklistId VARCHAR(16) NOT NULL, parentId VARCHAR(16) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_99EB20F9BA23A13 ON checklist_item (checklistId)'); + $this->addSql('CREATE INDEX IDX_99EB20F910EE4CEE ON checklist_item (parentId)'); + $this->addSql('CREATE INDEX IDX_99EB20F99D468A55 ON checklist_item (createTime)'); + $this->addSql('CREATE INDEX IDX_99EB20F955AA53E2 ON checklist_item (updateTime)'); + $this->addSql('ALTER TABLE checklist_item ADD CONSTRAINT FK_99EB20F9BA23A13 FOREIGN KEY (checklistId) REFERENCES checklist (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE checklist_item ADD CONSTRAINT FK_99EB20F910EE4CEE FOREIGN KEY (parentId) REFERENCES checklist_item (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE checklist_item DROP CONSTRAINT FK_99EB20F9BA23A13'); + $this->addSql('ALTER TABLE checklist_item DROP CONSTRAINT FK_99EB20F910EE4CEE'); + $this->addSql('DROP TABLE checklist_item'); + } +} diff --git a/api/src/Entity/Checklist.php b/api/src/Entity/Checklist.php index 5b839f2ff5..9a5ba7fd2c 100644 --- a/api/src/Entity/Checklist.php +++ b/api/src/Entity/Checklist.php @@ -16,6 +16,8 @@ use App\Repository\ChecklistRepository; use App\State\ChecklistCreateProcessor; use App\Util\EntityMap; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Validator\Constraints as Assert; @@ -82,6 +84,14 @@ class Checklist extends BaseEntity implements BelongsToCampInterface, CopyFromPr #[Groups(['create'])] public ?Checklist $copyChecklistSource; + /** + * All ChecklistItems that belong to this Checklist. + */ + #[ApiProperty(writable: false, example: '["/checklist_items/1a2b3c4d"]')] + #[Groups(['read'])] + #[ORM\OneToMany(targetEntity: ChecklistItem::class, mappedBy: 'checklist', cascade: ['persist'])] + public Collection $checklistItems; + /** * The human readable name of the checklist. */ @@ -96,12 +106,40 @@ class Checklist extends BaseEntity implements BelongsToCampInterface, CopyFromPr public function __construct() { parent::__construct(); + $this->checklistItems = new ArrayCollection(); } public function getCamp(): ?Camp { return $this->camp; } + /** + * @return ChecklistItem[] + */ + public function getChecklistItems(): array { + return $this->checklistItems->getValues(); + } + + public function addChecklistItem(ChecklistItem $checklistItem): self { + if (!$this->checklistItems->contains($checklistItem)) { + $this->checklistItems[] = $checklistItem; + $checklistItem->checklist = $this; + } + + return $this; + } + + public function removeChecklistItem(ChecklistItem $checklistItem): self { + if ($this->checklistItems->removeElement($checklistItem)) { + // set the owning side to null (unless already changed) + if ($checklistItem->checklist === $this) { + $checklistItem->checklist = null; + } + } + + return $this; + } + /** * @param Checklist $prototype * @param EntityMap $entityMap @@ -109,6 +147,19 @@ public function getCamp(): ?Camp { public function copyFromPrototype($prototype, $entityMap): void { $entityMap->add($prototype, $this); + // copy Checklist base properties $this->name = $prototype->name; + + // deep copy ChecklistItems + foreach ($prototype->getChecklistItems() as $checklistItemPrototype) { + // deep copy root ChecklistItems + // skip non-root ChecklistItems as these are copyed by there parent + if (null == $checklistItemPrototype->parent) { + $checklistItem = new ChecklistItem(); + $this->addChecklistItem($checklistItem); + + $checklistItem->copyFromPrototype($checklistItemPrototype, $entityMap); + } + } } } diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php new file mode 100644 index 0000000000..aee087b9a6 --- /dev/null +++ b/api/src/Entity/ChecklistItem.php @@ -0,0 +1,181 @@ + ['delete']] + ), + new GetCollection( + security: 'is_authenticated()' + ), + new Post( + denormalizationContext: ['groups' => ['write', 'create']], + securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' + ), + new GetCollection( + name: 'BelongsToChecklist_App\Entity\ChecklistItem_get_collection', + uriTemplate: self::CHECKLIST_SUBRESOURCE_URI_TEMPLATE, + uriVariables: [ + 'checklistId' => new Link( + fromClass: Checklist::class, + toProperty: 'checklist', + security: 'is_granted("CAMP_COLLABORATOR", checklist) or is_granted("CAMP_IS_PROTOTYPE", checklist)' + ), + ], + ), + ], + denormalizationContext: ['groups' => ['write']], + normalizationContext: ['groups' => ['read']], + order: ['checklist.id', 'id'], +)] +#[ApiFilter(filterClass: SearchFilter::class, properties: ['checklist'])] +#[ORM\Entity(repositoryClass: ChecklistItemRepository::class)] +class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface { + public const CHECKLIST_SUBRESOURCE_URI_TEMPLATE = '/checklists/{checklistId}/checklist_items.{_format}'; + + /** + * The Checklist this Item belongs to. + */ + #[ApiProperty(example: '/checklists/1a2b3c4d')] + #[Groups(['read', 'create'])] + #[ORM\ManyToOne(targetEntity: Checklist::class, inversedBy: 'checklistItems')] + #[ORM\JoinColumn(nullable: false, onDelete: 'cascade')] + public ?Checklist $checklist = null; + + /** + * The parent to which ChecklistItem item belongs. Is null in case this ChecklistItem is the + * root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long + * as the new parent is in the same checklist as the old one. + */ + #[AssertBelongsToChecklist(groups: ['update'])] + #[AssertNoLoop(groups: ['update'])] + #[ApiProperty(example: '/checklist_items/1a2b3c4d')] + #[Gedmo\SortableGroup] + #[Groups(['read', 'write'])] + #[ORM\ManyToOne(targetEntity: ChecklistItem::class, inversedBy: 'children')] + #[ORM\JoinColumn(onDelete: 'CASCADE')] + public ?ChecklistItem $parent = null; + + /** + * All ChecklistItems that are direct children of this ChecklistItem. + */ + #[ApiProperty(writable: false, example: '["/checklist_items/1a2b3c4d"]')] + #[Groups(['read'])] + #[ORM\OneToMany(targetEntity: ChecklistItem::class, mappedBy: 'parent', cascade: ['persist'])] + public Collection $children; + + /** + * The human readable text of the checklist-item. + */ + #[ApiProperty(example: 'Pfaditechnick')] + #[Groups(['read', 'write'])] + #[InputFilter\Trim] + #[InputFilter\CleanText] + #[Assert\NotBlank] + #[Assert\Length(max: 64)] + #[ORM\Column(type: 'text')] + public ?string $text = null; + + /** + * A whole number used for ordering multiple checklist items that are in the same parent. + * The API does not guarantee the uniqueness of parent+position. + */ + #[ApiProperty(example: '0')] + #[Gedmo\SortablePosition] + #[Groups(['read', 'write'])] + #[ORM\Column(type: 'integer', nullable: false)] + public int $position = -1; + + public function __construct() { + parent::__construct(); + $this->children = new ArrayCollection(); + } + + #[ApiProperty(readable: false)] + public function getCamp(): ?Camp { + return $this->checklist?->getCamp(); + } + + /** + * @return ChecklistItem[] + */ + public function getChildren(): array { + return $this->children->getValues(); + } + + public function addChild(self $child): self { + if (!$this->children->contains($child)) { + $this->children[] = $child; + $child->parent = $this; + } + + return $this; + } + + public function removeChild(self $child): self { + if ($this->children->removeElement($child)) { + // set the owning side to null (unless already changed) + if ($child->parent === $this) { + $child->parent = null; + } + } + + return $this; + } + + /** + * @param ChecklistItem $prototype + * @param EntityMap $entityMap + */ + public function copyFromPrototype($prototype, $entityMap): void { + $entityMap->add($prototype, $this); + + // copy ChecklistItem base properties + $this->text = $prototype->text; + + // deep copy ChecklistItems + foreach ($prototype->getChildren() as $childPrototype) { + $child = new ChecklistItem(); + $this->addChild($child); + $this->checklist->addChecklistItem($child); + + $child->copyFromPrototype($childPrototype, $entityMap); + } + } +} diff --git a/api/src/Repository/ChecklistItemRepository.php b/api/src/Repository/ChecklistItemRepository.php new file mode 100644 index 0000000000..b7e45f2d10 --- /dev/null +++ b/api/src/Repository/ChecklistItemRepository.php @@ -0,0 +1,37 @@ +getEntityManager()->createQueryBuilder(); + $checklistQry->select('c'); + $checklistQry->from(Checklist::class, 'c'); + $checklistQry->join(UserCamp::class, 'uc', Join::WITH, 'c.camp = uc.camp'); + $checklistQry->where('uc.user = :current_user'); + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $queryBuilder->andWhere($queryBuilder->expr()->in("{$rootAlias}.checklist", $checklistQry->getDQL())); + $queryBuilder->setParameter('current_user', $user); + } +} diff --git a/api/src/Validator/ChecklistItem/AssertBelongsToChecklist.php b/api/src/Validator/ChecklistItem/AssertBelongsToChecklist.php new file mode 100644 index 0000000000..ceb67ccc17 --- /dev/null +++ b/api/src/Validator/ChecklistItem/AssertBelongsToChecklist.php @@ -0,0 +1,10 @@ +context->getObject(); + if (!$object instanceof ChecklistItem) { + throw new UnexpectedValueException($object, ChecklistItem::class); + } + + if ($value->checklist->getId() !== $object->checklist->getId()) { + $this->context->buildViolation($constraint->message) + ->addViolation() + ; + } + } +} diff --git a/api/src/Validator/ChecklistItem/AssertNoLoop.php b/api/src/Validator/ChecklistItem/AssertNoLoop.php new file mode 100644 index 0000000000..f60e2bba67 --- /dev/null +++ b/api/src/Validator/ChecklistItem/AssertNoLoop.php @@ -0,0 +1,10 @@ +context->getObject(); + if (!$object instanceof ChecklistItem) { + throw new UnexpectedValueException($object, ChecklistItem::class); + } + + /** @var ChecklistItem $parent */ + $parent = $value; + + // $seen keeps track of all parents that we have visited. This is for a safety + // bailout mechanism to avoid an infinite loop in case there is flawed data in the DB + $seen = []; + + while (null !== $parent && !in_array($parent->getId(), $seen)) { + if ($parent->getId() === $object->getId()) { + $this->context->buildViolation($constraint->message) + ->addViolation() + ; + + return; + } + + $seen[] = $parent->getId(); + $parent = $parent->parent; + } + } +} diff --git a/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php b/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php index f434e34a8a..a734aa4518 100644 --- a/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php +++ b/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php @@ -12,6 +12,7 @@ public static function get(string $collectionEndpoint, array $fixtures): mixed { '/camps' => $fixtures['camp1'], '/categories' => $fixtures['category1'], '/checklists' => $fixtures['checklist1'], + '/checklist_items' => $fixtures['checklistItem1_1_1'], '/content_node/column_layouts' => $fixtures['columnLayout2'], '/content_node/responsive_layouts' => $fixtures['responsiveLayout1'], '/content_types' => $fixtures['contentTypeSafetyConcept'], diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklist_items__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklist_items__1.json new file mode 100644 index 0000000000..82a195c448 --- /dev/null +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklist_items__1.json @@ -0,0 +1,110 @@ +{ + "_embedded": { + "items": [ + { + "_links": { + "checklist": { + "href": "escaped_value" + }, + "children": [], + "parent": "escaped_value", + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "position": "escaped_value", + "text": "escaped_value" + }, + { + "_links": { + "checklist": { + "href": "escaped_value" + }, + "children": [], + "parent": "escaped_value", + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "position": "escaped_value", + "text": "escaped_value" + }, + { + "_links": { + "checklist": { + "href": "escaped_value" + }, + "children": [], + "parent": "escaped_value", + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "position": "escaped_value", + "text": "escaped_value" + }, + { + "_links": { + "checklist": { + "href": "escaped_value" + }, + "children": [], + "parent": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "position": "escaped_value", + "text": "escaped_value" + }, + { + "_links": { + "checklist": { + "href": "escaped_value" + }, + "children": [ + { + "href": "escaped_value" + } + ], + "parent": "escaped_value", + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "position": "escaped_value", + "text": "escaped_value" + } + ] + }, + "_links": { + "items": [ + { + "href": "escaped_value" + }, + { + "href": "escaped_value" + }, + { + "href": "escaped_value" + }, + { + "href": "escaped_value" + }, + { + "href": "escaped_value" + } + ], + "self": { + "href": "escaped_value" + } + }, + "totalItems": "escaped_value" +} diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklists__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklists__1.json index 16ae186f1b..feed0e6431 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklists__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklists__1.json @@ -6,6 +6,9 @@ "camp": { "href": "escaped_value" }, + "checklistItems": { + "href": "escaped_value" + }, "self": { "href": "escaped_value" } @@ -18,6 +21,9 @@ "camp": { "href": "escaped_value" }, + "checklistItems": { + "href": "escaped_value" + }, "self": { "href": "escaped_value" } @@ -30,6 +36,9 @@ "camp": { "href": "escaped_value" }, + "checklistItems": { + "href": "escaped_value" + }, "self": { "href": "escaped_value" } @@ -42,6 +51,9 @@ "camp": { "href": "escaped_value" }, + "checklistItems": { + "href": "escaped_value" + }, "self": { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklist_items__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklist_items__1.json new file mode 100644 index 0000000000..8a41227692 --- /dev/null +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklist_items__1.json @@ -0,0 +1,15 @@ +{ + "_links": { + "checklist": { + "href": "escaped_value" + }, + "children": [], + "parent": "escaped_value", + "self": { + "href": "escaped_value" + } + }, + "id": "escaped_value", + "position": "escaped_value", + "text": "escaped_value" +} diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklists__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklists__1.json index 39e7e3d07f..328e6ab533 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklists__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklists__1.json @@ -3,6 +3,9 @@ "camp": { "href": "escaped_value" }, + "checklistItems": { + "href": "escaped_value" + }, "self": { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 52c7add8a6..70dbc0ebc5 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -7623,6 +7623,15 @@ components: example: /camps/1a2b3c4d format: iri-reference type: string + checklistItems: + description: 'All ChecklistItems that belong to this Checklist.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d @@ -7636,6 +7645,7 @@ components: type: string required: - camp + - checklistItems - name type: object Checklist-write: @@ -7709,8 +7719,11 @@ components: properties: camp: properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + checklistItems: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } required: - camp + - checklistItems type: object type: type: string @@ -7726,6 +7739,8 @@ components: anyOf: - $ref: '#/components/schemas/Checklist.jsonapi' + - + $ref: '#/components/schemas/Checklist.jsonapi' readOnly: true type: array type: object @@ -7749,6 +7764,15 @@ components: example: /camps/1a2b3c4d format: iri-reference type: string + checklistItems: + description: 'All ChecklistItems that belong to this Checklist.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d @@ -7762,6 +7786,7 @@ components: type: string required: - camp + - checklistItems - name type: object Checklist.jsonhal-write_create: @@ -7834,6 +7859,15 @@ components: example: /camps/1a2b3c4d format: iri-reference type: string + checklistItems: + description: 'All ChecklistItems that belong to this Checklist.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d @@ -7847,6 +7881,7 @@ components: type: string required: - camp + - checklistItems - name type: object Checklist.jsonld-write_create: @@ -7873,8 +7908,408 @@ components: maxLength: 32 type: string required: - - camp - - name + - camp + - name + type: object + ChecklistItem-read: + deprecated: false + description: |- + A ChecklistItem + A Checklist contains a Tree-Structure of ChecklistItems. + properties: + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: string + children: + description: 'All ChecklistItems that are direct children of this ChecklistItem.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + parent: + description: |- + The parent to which ChecklistItem item belongs. Is null in case this ChecklistItem is the + root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long + as the new parent is in the same checklist as the old one. + example: /checklist_items/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: 'A whole number used for ordering multiple checklist items that are in the same parent.' + example: -1 + type: integer + text: + description: 'The human readable text of the checklist-item.' + example: Pfaditechnick + maxLength: 64 + type: string + required: + - checklist + - children + - position + - text + type: object + ChecklistItem-write: + deprecated: false + description: |- + A ChecklistItem + A Checklist contains a Tree-Structure of ChecklistItems. + properties: + parent: + description: |- + The parent to which ChecklistItem item belongs. Is null in case this ChecklistItem is the + root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long + as the new parent is in the same checklist as the old one. + example: /checklist_items/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: 'A whole number used for ordering multiple checklist items that are in the same parent.' + example: -1 + type: integer + text: + description: 'The human readable text of the checklist-item.' + example: Pfaditechnick + maxLength: 64 + type: string + required: + - position + - text + type: object + ChecklistItem-write_create: + deprecated: false + description: |- + A ChecklistItem + A Checklist contains a Tree-Structure of ChecklistItems. + properties: + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: string + parent: + description: |- + The parent to which ChecklistItem item belongs. Is null in case this ChecklistItem is the + root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long + as the new parent is in the same checklist as the old one. + example: /checklist_items/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: 'A whole number used for ordering multiple checklist items that are in the same parent.' + example: -1 + type: integer + text: + description: 'The human readable text of the checklist-item.' + example: Pfaditechnick + maxLength: 64 + type: string + required: + - checklist + - position + - text + type: object + ChecklistItem.jsonapi: + deprecated: false + description: |- + A ChecklistItem + A Checklist contains a Tree-Structure of ChecklistItems. + properties: + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + position: + default: -1 + description: 'A whole number used for ordering multiple checklist items that are in the same parent.' + example: -1 + type: integer + text: + description: 'The human readable text of the checklist-item.' + example: Pfaditechnick + maxLength: 64 + type: string + required: + - position + - text + type: object + id: + type: string + relationships: + properties: + checklist: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + children: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + parent: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + required: + - checklist + - children + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - + $ref: '#/components/schemas/ChecklistItem.jsonapi' + - + $ref: '#/components/schemas/ChecklistItem.jsonapi' + - + $ref: '#/components/schemas/ChecklistItem.jsonapi' + readOnly: true + type: array + type: object + ChecklistItem.jsonhal-read: + deprecated: false + description: |- + A ChecklistItem + A Checklist contains a Tree-Structure of ChecklistItems. + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: string + children: + description: 'All ChecklistItems that are direct children of this ChecklistItem.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + parent: + description: |- + The parent to which ChecklistItem item belongs. Is null in case this ChecklistItem is the + root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long + as the new parent is in the same checklist as the old one. + example: /checklist_items/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: 'A whole number used for ordering multiple checklist items that are in the same parent.' + example: -1 + type: integer + text: + description: 'The human readable text of the checklist-item.' + example: Pfaditechnick + maxLength: 64 + type: string + required: + - checklist + - children + - position + - text + type: object + ChecklistItem.jsonhal-write_create: + deprecated: false + description: |- + A ChecklistItem + A Checklist contains a Tree-Structure of ChecklistItems. + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: string + parent: + description: |- + The parent to which ChecklistItem item belongs. Is null in case this ChecklistItem is the + root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long + as the new parent is in the same checklist as the old one. + example: /checklist_items/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: 'A whole number used for ordering multiple checklist items that are in the same parent.' + example: -1 + type: integer + text: + description: 'The human readable text of the checklist-item.' + example: Pfaditechnick + maxLength: 64 + type: string + required: + - checklist + - position + - text + type: object + ChecklistItem.jsonld-read: + deprecated: false + description: |- + A ChecklistItem + A Checklist contains a Tree-Structure of ChecklistItems. + properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: string + children: + description: 'All ChecklistItems that are direct children of this ChecklistItem.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + parent: + description: |- + The parent to which ChecklistItem item belongs. Is null in case this ChecklistItem is the + root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long + as the new parent is in the same checklist as the old one. + example: /checklist_items/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: 'A whole number used for ordering multiple checklist items that are in the same parent.' + example: -1 + type: integer + text: + description: 'The human readable text of the checklist-item.' + example: Pfaditechnick + maxLength: 64 + type: string + required: + - checklist + - children + - position + - text + type: object + ChecklistItem.jsonld-write_create: + deprecated: false + description: |- + A ChecklistItem + A Checklist contains a Tree-Structure of ChecklistItems. + properties: + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: string + parent: + description: |- + The parent to which ChecklistItem item belongs. Is null in case this ChecklistItem is the + root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long + as the new parent is in the same checklist as the old one. + example: /checklist_items/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: 'A whole number used for ordering multiple checklist items that are in the same parent.' + example: -1 + type: integer + text: + description: 'The human readable text of the checklist-item.' + example: Pfaditechnick + maxLength: 64 + type: string + required: + - checklist + - position + - text type: object ColumnLayout-read: deprecated: false @@ -23176,59 +23611,308 @@ paths: type: array style: form responses: - 200: + 200: + content: + application/hal+json: + schema: + properties: + _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/Checklist.jsonhal-read' }, type: array } }, type: object }, { type: object }] } + _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } + itemsPerPage: { minimum: 0, type: integer } + totalItems: { minimum: 0, type: integer } + required: + - _embedded + - _links + type: object + application/json: + schema: + items: + $ref: '#/components/schemas/Checklist-read' + type: array + application/ld+json: + schema: + properties: + 'hydra:member': { items: { $ref: '#/components/schemas/Checklist.jsonld-read' }, type: array } + 'hydra:search': { properties: { '@type': { type: string }, 'hydra:mapping': { items: { properties: { '@type': { type: string }, property: { type: ['null', string] }, required: { type: boolean }, variable: { type: string } }, type: object }, type: array }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string } }, type: object } + 'hydra:totalItems': { minimum: 0, type: integer } + 'hydra:view': { example: { '@id': string, 'hydra:first': string, 'hydra:last': string, 'hydra:next': string, 'hydra:previous': string, type: string }, properties: { '@id': { format: iri-reference, type: string }, '@type': { type: string }, 'hydra:first': { format: iri-reference, type: string }, 'hydra:last': { format: iri-reference, type: string }, 'hydra:next': { format: iri-reference, type: string }, 'hydra:previous': { format: iri-reference, type: string } }, type: object } + required: + - 'hydra:member' + type: object + application/vnd.api+json: + schema: + items: + $ref: '#/components/schemas/Checklist.jsonapi' + type: array + text/html: + schema: + items: + $ref: '#/components/schemas/Checklist-read' + type: array + description: 'Checklist collection' + summary: 'Retrieves the collection of Checklist resources.' + tags: + - Checklist + '/camps/{id}': + delete: + deprecated: false + description: 'Removes the Camp resource.' + operationId: api_camps_id_delete + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'Camp identifier' + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + 204: + description: 'Camp resource deleted' + 404: + description: 'Resource not found' + summary: 'Removes the Camp resource.' + tags: + - Camp + get: + deprecated: false + description: 'Retrieves a Camp resource.' + operationId: api_camps_id_get + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'Camp identifier' + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + 200: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/Camp.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + application/json: + schema: + $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + application/ld+json: + schema: + $ref: '#/components/schemas/Camp.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Camp.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + description: 'Camp resource' + 404: + description: 'Resource not found' + summary: 'Retrieves a Camp resource.' + tags: + - Camp + patch: + deprecated: false + description: 'Updates the Camp resource.' + operationId: api_camps_id_patch + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'Camp identifier' + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/Camp-write_update' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Camp.jsonapi' + description: 'The updated Camp resource' + required: true + responses: + 200: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/Camp.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + application/json: + schema: + $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + application/ld+json: + schema: + $ref: '#/components/schemas/Camp.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Camp.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + description: 'Camp resource updated' + links: [] + 400: + description: 'Invalid input' + 404: + description: 'Resource not found' + 422: + description: 'Unprocessable entity' + summary: 'Updates the Camp resource.' + tags: + - Camp + /categories: + get: + deprecated: false + description: 'Retrieves the collection of Category resources.' + operationId: api_categories_get_collection + parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: true + in: query + name: 'camp[]' + required: false + schema: + items: + type: string + type: array + style: form + responses: + 200: + content: + application/hal+json: + schema: + properties: + _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/Category.jsonhal-read' }, type: array } }, type: object }, { type: object }] } + _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } + itemsPerPage: { minimum: 0, type: integer } + totalItems: { minimum: 0, type: integer } + required: + - _embedded + - _links + type: object + application/json: + schema: + items: + $ref: '#/components/schemas/Category-read' + type: array + application/ld+json: + schema: + properties: + 'hydra:member': { items: { $ref: '#/components/schemas/Category.jsonld-read' }, type: array } + 'hydra:search': { properties: { '@type': { type: string }, 'hydra:mapping': { items: { properties: { '@type': { type: string }, property: { type: ['null', string] }, required: { type: boolean }, variable: { type: string } }, type: object }, type: array }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string } }, type: object } + 'hydra:totalItems': { minimum: 0, type: integer } + 'hydra:view': { example: { '@id': string, 'hydra:first': string, 'hydra:last': string, 'hydra:next': string, 'hydra:previous': string, type: string }, properties: { '@id': { format: iri-reference, type: string }, '@type': { type: string }, 'hydra:first': { format: iri-reference, type: string }, 'hydra:last': { format: iri-reference, type: string }, 'hydra:next': { format: iri-reference, type: string }, 'hydra:previous': { format: iri-reference, type: string } }, type: object } + required: + - 'hydra:member' + type: object + application/vnd.api+json: + schema: + items: + $ref: '#/components/schemas/Category.jsonapi' + type: array + text/html: + schema: + items: + $ref: '#/components/schemas/Category-read' + type: array + description: 'Category collection' + summary: 'Retrieves the collection of Category resources.' + tags: + - Category + post: + deprecated: false + description: 'Creates a Category resource.' + operationId: api_categories_post + parameters: [] + requestBody: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/Category.jsonhal-write_create' + application/json: + schema: + $ref: '#/components/schemas/Category-write_create' + application/ld+json: + schema: + $ref: '#/components/schemas/Category.jsonld-write_create' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/Category.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/Category-write_create' + description: 'The new Category resource' + required: true + responses: + 201: content: application/hal+json: schema: - properties: - _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/Checklist.jsonhal-read' }, type: array } }, type: object }, { type: object }] } - _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } - itemsPerPage: { minimum: 0, type: integer } - totalItems: { minimum: 0, type: integer } - required: - - _embedded - - _links - type: object + $ref: '#/components/schemas/Category.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' application/json: schema: - items: - $ref: '#/components/schemas/Checklist-read' - type: array + $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' application/ld+json: schema: - properties: - 'hydra:member': { items: { $ref: '#/components/schemas/Checklist.jsonld-read' }, type: array } - 'hydra:search': { properties: { '@type': { type: string }, 'hydra:mapping': { items: { properties: { '@type': { type: string }, property: { type: ['null', string] }, required: { type: boolean }, variable: { type: string } }, type: object }, type: array }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string } }, type: object } - 'hydra:totalItems': { minimum: 0, type: integer } - 'hydra:view': { example: { '@id': string, 'hydra:first': string, 'hydra:last': string, 'hydra:next': string, 'hydra:previous': string, type: string }, properties: { '@id': { format: iri-reference, type: string }, '@type': { type: string }, 'hydra:first': { format: iri-reference, type: string }, 'hydra:last': { format: iri-reference, type: string }, 'hydra:next': { format: iri-reference, type: string }, 'hydra:previous': { format: iri-reference, type: string } }, type: object } - required: - - 'hydra:member' - type: object + $ref: '#/components/schemas/Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' application/vnd.api+json: schema: - items: - $ref: '#/components/schemas/Checklist.jsonapi' - type: array + $ref: '#/components/schemas/Category.jsonapi' text/html: schema: - items: - $ref: '#/components/schemas/Checklist-read' - type: array - description: 'Checklist collection' - summary: 'Retrieves the collection of Checklist resources.' + $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' + description: 'Category resource created' + links: [] + 400: + description: 'Invalid input' + 422: + description: 'Unprocessable entity' + summary: 'Creates a Category resource.' tags: - - Checklist - '/camps/{id}': + - Category + '/categories/{id}': delete: deprecated: false - description: 'Removes the Camp resource.' - operationId: api_camps_id_delete + description: 'Removes the Category resource.' + operationId: api_categories_id_delete parameters: - allowEmptyValue: false allowReserved: false deprecated: false - description: 'Camp identifier' + description: 'Category identifier' explode: false in: path name: id @@ -23238,22 +23922,22 @@ paths: style: simple responses: 204: - description: 'Camp resource deleted' + description: 'Category resource deleted' 404: description: 'Resource not found' - summary: 'Removes the Camp resource.' + summary: 'Removes the Category resource.' tags: - - Camp + - Category get: deprecated: false - description: 'Retrieves a Camp resource.' - operationId: api_camps_id_get + description: 'Retrieves a Category resource.' + operationId: api_categories_id_get parameters: - allowEmptyValue: false allowReserved: false deprecated: false - description: 'Camp identifier' + description: 'Category identifier' explode: false in: path name: id @@ -23266,35 +23950,35 @@ paths: content: application/hal+json: schema: - $ref: '#/components/schemas/Camp.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Category.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' application/json: schema: - $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' application/ld+json: schema: - $ref: '#/components/schemas/Camp.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' application/vnd.api+json: schema: - $ref: '#/components/schemas/Camp.jsonapi' + $ref: '#/components/schemas/Category.jsonapi' text/html: schema: - $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - description: 'Camp resource' + $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' + description: 'Category resource' 404: description: 'Resource not found' - summary: 'Retrieves a Camp resource.' + summary: 'Retrieves a Category resource.' tags: - - Camp + - Category patch: deprecated: false - description: 'Updates the Camp resource.' - operationId: api_camps_id_patch + description: 'Updates the Category resource.' + operationId: api_categories_id_patch parameters: - allowEmptyValue: false allowReserved: false deprecated: false - description: 'Camp identifier' + description: 'Category identifier' explode: false in: path name: id @@ -23306,31 +23990,31 @@ paths: content: application/merge-patch+json: schema: - $ref: '#/components/schemas/Camp-write_update' + $ref: '#/components/schemas/Category-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/Camp.jsonapi' - description: 'The updated Camp resource' + $ref: '#/components/schemas/Category.jsonapi' + description: 'The updated Category resource' required: true responses: 200: content: application/hal+json: schema: - $ref: '#/components/schemas/Camp.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Category.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' application/json: schema: - $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' application/ld+json: schema: - $ref: '#/components/schemas/Camp.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' application/vnd.api+json: schema: - $ref: '#/components/schemas/Camp.jsonapi' + $ref: '#/components/schemas/Category.jsonapi' text/html: schema: - $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - description: 'Camp resource updated' + $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' + description: 'Category resource updated' links: [] 400: description: 'Invalid input' @@ -23338,14 +24022,14 @@ paths: description: 'Resource not found' 422: description: 'Unprocessable entity' - summary: 'Updates the Camp resource.' + summary: 'Updates the Category resource.' tags: - - Camp - /categories: + - Category + /checklist_items: get: deprecated: false - description: 'Retrieves the collection of Category resources.' - operationId: api_categories_get_collection + description: 'Retrieves the collection of ChecklistItem resources.' + operationId: api_checklist_items_get_collection parameters: - allowEmptyValue: true @@ -23354,7 +24038,7 @@ paths: description: '' explode: false in: query - name: camp + name: checklist required: false schema: type: string @@ -23366,7 +24050,7 @@ paths: description: '' explode: true in: query - name: 'camp[]' + name: 'checklist[]' required: false schema: items: @@ -23379,7 +24063,7 @@ paths: application/hal+json: schema: properties: - _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/Category.jsonhal-read' }, type: array } }, type: object }, { type: object }] } + _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/ChecklistItem.jsonhal-read' }, type: array } }, type: object }, { type: object }] } _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } itemsPerPage: { minimum: 0, type: integer } totalItems: { minimum: 0, type: integer } @@ -23390,12 +24074,12 @@ paths: application/json: schema: items: - $ref: '#/components/schemas/Category-read' + $ref: '#/components/schemas/ChecklistItem-read' type: array application/ld+json: schema: properties: - 'hydra:member': { items: { $ref: '#/components/schemas/Category.jsonld-read' }, type: array } + 'hydra:member': { items: { $ref: '#/components/schemas/ChecklistItem.jsonld-read' }, type: array } 'hydra:search': { properties: { '@type': { type: string }, 'hydra:mapping': { items: { properties: { '@type': { type: string }, property: { type: ['null', string] }, required: { type: boolean }, variable: { type: string } }, type: object }, type: array }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string } }, type: object } 'hydra:totalItems': { minimum: 0, type: integer } 'hydra:view': { example: { '@id': string, 'hydra:first': string, 'hydra:last': string, 'hydra:next': string, 'hydra:previous': string, type: string }, properties: { '@id': { format: iri-reference, type: string }, '@type': { type: string }, 'hydra:first': { format: iri-reference, type: string }, 'hydra:last': { format: iri-reference, type: string }, 'hydra:next': { format: iri-reference, type: string }, 'hydra:previous': { format: iri-reference, type: string } }, type: object } @@ -23405,79 +24089,80 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/Category.jsonapi' + $ref: '#/components/schemas/ChecklistItem.jsonapi' type: array text/html: schema: items: - $ref: '#/components/schemas/Category-read' + $ref: '#/components/schemas/ChecklistItem-read' type: array - description: 'Category collection' - summary: 'Retrieves the collection of Category resources.' + description: 'ChecklistItem collection' + summary: 'Retrieves the collection of ChecklistItem resources.' tags: - - Category + - ChecklistItem + parameters: [] post: deprecated: false - description: 'Creates a Category resource.' - operationId: api_categories_post + description: 'Creates a ChecklistItem resource.' + operationId: api_checklist_items_post parameters: [] requestBody: content: application/hal+json: schema: - $ref: '#/components/schemas/Category.jsonhal-write_create' + $ref: '#/components/schemas/ChecklistItem.jsonhal-write_create' application/json: schema: - $ref: '#/components/schemas/Category-write_create' + $ref: '#/components/schemas/ChecklistItem-write_create' application/ld+json: schema: - $ref: '#/components/schemas/Category.jsonld-write_create' + $ref: '#/components/schemas/ChecklistItem.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi' + $ref: '#/components/schemas/ChecklistItem.jsonapi' text/html: schema: - $ref: '#/components/schemas/Category-write_create' - description: 'The new Category resource' + $ref: '#/components/schemas/ChecklistItem-write_create' + description: 'The new ChecklistItem resource' required: true responses: 201: content: application/hal+json: schema: - $ref: '#/components/schemas/Category.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/ChecklistItem.jsonhal-read' application/json: schema: - $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/ChecklistItem-read' application/ld+json: schema: - $ref: '#/components/schemas/Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/ChecklistItem.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi' + $ref: '#/components/schemas/ChecklistItem.jsonapi' text/html: schema: - $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' - description: 'Category resource created' + $ref: '#/components/schemas/ChecklistItem-read' + description: 'ChecklistItem resource created' links: [] 400: description: 'Invalid input' 422: description: 'Unprocessable entity' - summary: 'Creates a Category resource.' + summary: 'Creates a ChecklistItem resource.' tags: - - Category - '/categories/{id}': + - ChecklistItem + '/checklist_items/{id}': delete: deprecated: false - description: 'Removes the Category resource.' - operationId: api_categories_id_delete + description: 'Removes the ChecklistItem resource.' + operationId: api_checklist_items_id_delete parameters: - allowEmptyValue: false allowReserved: false deprecated: false - description: 'Category identifier' + description: 'ChecklistItem identifier' explode: false in: path name: id @@ -23487,22 +24172,22 @@ paths: style: simple responses: 204: - description: 'Category resource deleted' + description: 'ChecklistItem resource deleted' 404: description: 'Resource not found' - summary: 'Removes the Category resource.' + summary: 'Removes the ChecklistItem resource.' tags: - - Category + - ChecklistItem get: deprecated: false - description: 'Retrieves a Category resource.' - operationId: api_categories_id_get + description: 'Retrieves a ChecklistItem resource.' + operationId: api_checklist_items_id_get parameters: - allowEmptyValue: false allowReserved: false deprecated: false - description: 'Category identifier' + description: 'ChecklistItem identifier' explode: false in: path name: id @@ -23515,35 +24200,36 @@ paths: content: application/hal+json: schema: - $ref: '#/components/schemas/Category.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/ChecklistItem.jsonhal-read' application/json: schema: - $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/ChecklistItem-read' application/ld+json: schema: - $ref: '#/components/schemas/Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/ChecklistItem.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi' + $ref: '#/components/schemas/ChecklistItem.jsonapi' text/html: schema: - $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' - description: 'Category resource' + $ref: '#/components/schemas/ChecklistItem-read' + description: 'ChecklistItem resource' 404: description: 'Resource not found' - summary: 'Retrieves a Category resource.' + summary: 'Retrieves a ChecklistItem resource.' tags: - - Category + - ChecklistItem + parameters: [] patch: deprecated: false - description: 'Updates the Category resource.' - operationId: api_categories_id_patch + description: 'Updates the ChecklistItem resource.' + operationId: api_checklist_items_id_patch parameters: - allowEmptyValue: false allowReserved: false deprecated: false - description: 'Category identifier' + description: 'ChecklistItem identifier' explode: false in: path name: id @@ -23555,31 +24241,31 @@ paths: content: application/merge-patch+json: schema: - $ref: '#/components/schemas/Category-write_update' + $ref: '#/components/schemas/ChecklistItem-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi' - description: 'The updated Category resource' + $ref: '#/components/schemas/ChecklistItem.jsonapi' + description: 'The updated ChecklistItem resource' required: true responses: 200: content: application/hal+json: schema: - $ref: '#/components/schemas/Category.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/ChecklistItem.jsonhal-read' application/json: schema: - $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/ChecklistItem-read' application/ld+json: schema: - $ref: '#/components/schemas/Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/ChecklistItem.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi' + $ref: '#/components/schemas/ChecklistItem.jsonapi' text/html: schema: - $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' - description: 'Category resource updated' + $ref: '#/components/schemas/ChecklistItem-read' + description: 'ChecklistItem resource updated' links: [] 400: description: 'Invalid input' @@ -23587,9 +24273,9 @@ paths: description: 'Resource not found' 422: description: 'Unprocessable entity' - summary: 'Updates the Category resource.' + summary: 'Updates the ChecklistItem resource.' tags: - - Category + - ChecklistItem /checklists: get: deprecated: false @@ -23716,6 +24402,94 @@ paths: summary: 'Creates a Checklist resource.' tags: - Checklist + '/checklists/{checklistId}/checklist_items': + get: + deprecated: false + description: 'Retrieves the collection of ChecklistItem resources.' + operationId: BelongsToChecklist_App\Entity\ChecklistItem_get_collection + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'ChecklistItem identifier' + explode: false + in: path + name: checklistId + required: true + schema: + type: string + style: simple + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: checklist + required: false + schema: + type: string + style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: true + in: query + name: 'checklist[]' + required: false + schema: + items: + type: string + type: array + style: form + responses: + 200: + content: + application/hal+json: + schema: + properties: + _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/ChecklistItem.jsonhal-read' }, type: array } }, type: object }, { type: object }] } + _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } + itemsPerPage: { minimum: 0, type: integer } + totalItems: { minimum: 0, type: integer } + required: + - _embedded + - _links + type: object + application/json: + schema: + items: + $ref: '#/components/schemas/ChecklistItem-read' + type: array + application/ld+json: + schema: + properties: + 'hydra:member': { items: { $ref: '#/components/schemas/ChecklistItem.jsonld-read' }, type: array } + 'hydra:search': { properties: { '@type': { type: string }, 'hydra:mapping': { items: { properties: { '@type': { type: string }, property: { type: ['null', string] }, required: { type: boolean }, variable: { type: string } }, type: object }, type: array }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string } }, type: object } + 'hydra:totalItems': { minimum: 0, type: integer } + 'hydra:view': { example: { '@id': string, 'hydra:first': string, 'hydra:last': string, 'hydra:next': string, 'hydra:previous': string, type: string }, properties: { '@id': { format: iri-reference, type: string }, '@type': { type: string }, 'hydra:first': { format: iri-reference, type: string }, 'hydra:last': { format: iri-reference, type: string }, 'hydra:next': { format: iri-reference, type: string }, 'hydra:previous': { format: iri-reference, type: string } }, type: object } + required: + - 'hydra:member' + type: object + application/vnd.api+json: + schema: + items: + $ref: '#/components/schemas/ChecklistItem.jsonapi' + type: array + text/html: + schema: + items: + $ref: '#/components/schemas/ChecklistItem-read' + type: array + description: 'ChecklistItem collection' + summary: 'Retrieves the collection of ChecklistItem resources.' + tags: + - ChecklistItem + parameters: [] '/checklists/{id}': delete: deprecated: false diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json index 84921e20f9..08bdba78ce 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json @@ -24,6 +24,10 @@ "href": "\/categories{\/id}{?camp,camp[]}", "templated": true }, + "checklistItems": { + "href": "\/checklist_items{\/id}{?checklist,checklist[]}", + "templated": true + }, "checklists": { "href": "\/checklists{\/id}{?camp,camp[]}", "templated": true diff --git a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml index 71b7a0fd66..cffd038675 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml @@ -12,6 +12,8 @@ /categories/item: 9 /checklists: 6 /checklists/item: 7 +/checklist_items: 6 +/checklist_items/item: 8 /content_types: 6 /content_types/item: 6 /days: 26 From e2cdf141bcd5296a4eb09a0fed0894f11222a743 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Sat, 15 Jun 2024 23:22:07 +0200 Subject: [PATCH 06/30] add Unittests for ChecklistItem --- .../CreateChecklistItemTest.php | 260 ++++++++++++++++++ .../DeleteChecklistItemTest.php | 87 ++++++ .../ChecklistItems/ListChecklistItemTest.php | 128 +++++++++ .../ChecklistItems/ReadChecklistItemTest.php | 108 ++++++++ .../UpdateChecklistItemTest.php | 220 +++++++++++++++ 5 files changed, 803 insertions(+) create mode 100644 api/tests/Api/ChecklistItems/CreateChecklistItemTest.php create mode 100644 api/tests/Api/ChecklistItems/DeleteChecklistItemTest.php create mode 100644 api/tests/Api/ChecklistItems/ListChecklistItemTest.php create mode 100644 api/tests/Api/ChecklistItems/ReadChecklistItemTest.php create mode 100644 api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php diff --git a/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php b/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php new file mode 100644 index 0000000000..007d8d0970 --- /dev/null +++ b/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php @@ -0,0 +1,260 @@ +request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload()]); + + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testCreateChecklistItemIsNotPossibleForUnrelatedUserBecauseCampIsNotReadable() { + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload()]) + ; + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Item not found for "'.$this->getIriFor('checklist1').'".', + ]); + } + + public function testCreateChecklistItemIsNotPossibleForInactiveCollaboratorBecauseCampIsNotReadable() { + static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload()]) + ; + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Item not found for "'.$this->getIriFor('checklist1').'".', + ]); + } + + public function testCreateChecklistItemIsDeniedForGuest() { + static::createClientWithCredentials(['email' => static::$fixtures['user3guest']->getEmail()]) + ->request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload()]) + ; + + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testCreateChecklistItemIsAllowedForMember() { + static::createClientWithCredentials(['email' => static::$fixtures['user2member']->getEmail()]) + ->request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload()]) + ; + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains($this->getExampleReadPayload(['position' => 5])); + } + + public function testCreateChecklistItemIsAllowedForManager() { + static::createClientWithCredentials()->request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload()]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains($this->getExampleReadPayload(['position' => 5])); + } + + public function testCreateChecklistItemInCampPrototypeIsDeniedForUnrelatedUser() { + static::createClientWithCredentials()->request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload([ + 'checklist' => $this->getIriFor('checklist1campPrototype'), + ])]); + + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testCreateChecklistItemValidatesMissingChecklist() { + static::createClientWithCredentials()->request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload([], ['checklist'])]); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'checklist', + 'message' => 'This value should not be null.', + ], + ], + ]); + } + + public function testCreateChecklistItemValidatesMissingText() { + static::createClientWithCredentials()->request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload([], ['text'])]); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'text', + 'message' => 'This value should not be blank.', + ], + ], + ]); + } + + public function testCreateChecklistItemValidatesBlankText() { + static::createClientWithCredentials()->request( + 'POST', + '/checklist_items', + [ + 'json' => $this->getExampleWritePayload( + [ + 'text' => '', + ] + ), + ] + ); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'text', + 'message' => 'This value should not be blank.', + ], + ], + ]); + } + + public function testCreateChecklistItemValidatesTooLongText() { + static::createClientWithCredentials()->request( + 'POST', + '/checklist_items', + [ + 'json' => $this->getExampleWritePayload( + [ + 'text' => str_repeat('l', 65), + ] + ), + ] + ); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'text', + 'message' => 'This value is too long. It should have 64 characters or less.', + ], + ], + ]); + } + + public function testCreateChecklistItemTrimsText() { + static::createClientWithCredentials()->request( + 'POST', + '/checklist_items', + [ + 'json' => $this->getExampleWritePayload( + [ + 'text' => " \t Ziel 1\t ", + ] + ), + ] + ); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains($this->getExampleReadPayload( + [ + 'text' => 'Ziel 1', + 'position' => 5, + ] + )); + } + + public function testCreateChecklistItemCleansForbiddenCharactersFromText() { + static::createClientWithCredentials()->request( + 'POST', + '/checklist_items', + [ + 'json' => $this->getExampleWritePayload( + [ + 'text' => "\n\tZiel 1", + ] + ), + ] + ); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains($this->getExampleReadPayload( + [ + 'text' => 'Ziel 1', + 'position' => 5, + ] + )); + } + + /** + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + */ + public function testCreateResponseStructureMatchesReadResponseStructure() { + $client = static::createClientWithCredentials(); + $client->disableReboot(); + $createResponse = $client->request( + 'POST', + '/checklist_items', + [ + 'json' => $this->getExampleWritePayload(), + ] + ); + + $this->assertResponseStatusCodeSame(201); + + $createArray = $createResponse->toArray(); + $newItemLink = $createArray['_links']['self']['href']; + $getItemResponse = $client->request('GET', $newItemLink); + + assertThat($createArray, CompatibleHalResponse::isHalCompatibleWith($getItemResponse->toArray())); + } + + public function getExampleWritePayload($attributes = [], $except = []) { + return $this->getExamplePayload( + ChecklistItem::class, + Post::class, + array_merge([ + 'parent' => null, + 'checklist' => $this->getIriFor('checklist1'), + ], $attributes), + [], + $except + ); + } + + public function getExampleReadPayload($attributes = [], $except = []) { + return $this->getExamplePayload( + ChecklistItem::class, + Get::class, + $attributes, + ['parent', 'checklist'], + $except + ); + } +} diff --git a/api/tests/Api/ChecklistItems/DeleteChecklistItemTest.php b/api/tests/Api/ChecklistItems/DeleteChecklistItemTest.php new file mode 100644 index 0000000000..262ef91457 --- /dev/null +++ b/api/tests/Api/ChecklistItems/DeleteChecklistItemTest.php @@ -0,0 +1,87 @@ +request('DELETE', '/checklist_items/'.$checklistItem->getId()); + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testDeleteChecklistItemIsDeniedForUnrelatedUser() { + $checklistItem = static::getFixture('checklistItem1_1_2_3'); + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('DELETE', '/checklist_items/'.$checklistItem->getId()) + ; + + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testDeleteChecklistItemIsDeniedForInactiveCollaborator() { + $checklistItem = static::getFixture('checklistItem1_1_2_3'); + static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('DELETE', '/checklist_items/'.$checklistItem->getId()) + ; + + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testDeleteChecklistItemIsDeniedForGuest() { + $checklistItem = static::getFixture('checklistItem1_1_2_3'); + static::createClientWithCredentials(['email' => static::$fixtures['user3guest']->getEmail()]) + ->request('DELETE', '/checklist_items/'.$checklistItem->getId()) + ; + + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testDeleteChecklistItemIsAllowedForMember() { + $checklistItem = static::getFixture('checklistItem1_1_2_3'); + static::createClientWithCredentials(['email' => static::$fixtures['user2member']->getEmail()]) + ->request('DELETE', '/checklist_items/'.$checklistItem->getId()) + ; + $this->assertResponseStatusCodeSame(204); + $this->assertNull($this->getEntityManager()->getRepository(ChecklistItem::class)->find($checklistItem->getId())); + } + + public function testDeleteChecklistItemIsAllowedForManager() { + $checklistItem = static::getFixture('checklistItem1_1_2_3'); + static::createClientWithCredentials()->request('DELETE', '/checklist_items/'.$checklistItem->getId()); + $this->assertResponseStatusCodeSame(204); + $this->assertNull($this->getEntityManager()->getRepository(ChecklistItem::class)->find($checklistItem->getId())); + } + + public function testDeleteChecklistItemFromCampPrototypeIsDeniedForUnrelatedUser() { + $checklistItem = static::getFixture('checklistItemPrototype_1_1'); + static::createClientWithCredentials()->request('DELETE', '/checklist_items/'.$checklistItem->getId()); + + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } +} diff --git a/api/tests/Api/ChecklistItems/ListChecklistItemTest.php b/api/tests/Api/ChecklistItems/ListChecklistItemTest.php new file mode 100644 index 0000000000..cbffa0d0a5 --- /dev/null +++ b/api/tests/Api/ChecklistItems/ListChecklistItemTest.php @@ -0,0 +1,128 @@ +request('GET', '/checklist_items'); + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testListChecklistItemsIsAllowedForLoggedInUserButFiltered() { + // precondition: There is a checklist-item that the user doesn't have access to + $this->assertNotEmpty(static::$fixtures['checklistItemUnrelated_1_1']); + + $response = static::createClientWithCredentials()->request('GET', '/checklist_items'); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'totalItems' => 5, + '_links' => [ + 'items' => [], + ], + '_embedded' => [ + 'items' => [], + ], + ]); + $this->assertEqualsCanonicalizing([ + ['href' => $this->getIriFor('checklistItem1_1_1')], + ['href' => $this->getIriFor('checklistItem1_1_2')], + ['href' => $this->getIriFor('checklistItem1_1_2_3')], + ['href' => $this->getIriFor('checklistItem2_1_1')], + ['href' => $this->getIriFor('checklistItemPrototype_1_1')], + ], $response->toArray()['_links']['items']); + } + + public function testListChecklistItemsFilteredByChecklistIsAllowedForCollaborator() { + $checklist = static::getFixture('checklist1'); + $response = static::createClientWithCredentials()->request('GET', '/checklist_items?checklist=%2Fchecklists%2F'.$checklist->getId()); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'totalItems' => 3, + '_links' => [ + 'items' => [], + ], + '_embedded' => [ + 'items' => [], + ], + ]); + $this->assertEqualsCanonicalizing([ + ['href' => $this->getIriFor('checklistItem1_1_1')], + ['href' => $this->getIriFor('checklistItem1_1_2')], + ['href' => $this->getIriFor('checklistItem1_1_2_3')], + ], $response->toArray()['_links']['items']); + } + + public function testListChecklistItemsFilteredByChecklistIsDeniedForUnrelatedUser() { + $checklist = static::getFixture('checklist1'); + $response = static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('GET', '/checklist_items?checklist=%2Fchecklists%2F'.$checklist->getId()) + ; + + $this->assertResponseStatusCodeSame(200); + + $this->assertJsonContains(['totalItems' => 0]); + $this->assertArrayNotHasKey('items', $response->toArray()['_links']); + } + + public function testListChecklistItemsFilteredByChecklistIsDeniedForInactiveCollaborator() { + $checklist = static::getFixture('checklist1'); + $response = static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('GET', '/checklist_items?checklist=%2Fchecklists%2F'.$checklist->getId()) + ; + + $this->assertResponseStatusCodeSame(200); + + $this->assertJsonContains(['totalItems' => 0]); + $this->assertArrayNotHasKey('items', $response->toArray()['_links']); + } + + public function testListChecklistItemsFilteredByChecklistPrototypeIsAllowedForUnrelatedUser() { + $checklist = static::getFixture('checklist1campPrototype'); + $response = static::createClientWithCredentials()->request('GET', '/checklist_items?checklist=%2Fchecklists%2F'.$checklist->getId()); + + $this->assertResponseStatusCodeSame(200); + + $this->assertJsonContains(['totalItems' => 1]); + $this->assertEqualsCanonicalizing([ + ['href' => $this->getIriFor('checklistItemPrototype_1_1')], + ], $response->toArray()['_links']['items']); + } + + public function testListChecklistItemsAsChecklistSubresourceIsAllowedForCollaborator() { + $checklist = static::getFixture('checklist1'); + $response = static::createClientWithCredentials()->request('GET', '/checklists/'.$checklist->getId().'/checklist_items'); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'totalItems' => 3, + '_links' => [ + 'items' => [], + ], + '_embedded' => [ + 'items' => [], + ], + ]); + $this->assertEqualsCanonicalizing([ + ['href' => $this->getIriFor('checklistItem1_1_1')], + ['href' => $this->getIriFor('checklistItem1_1_2')], + ['href' => $this->getIriFor('checklistItem1_1_2_3')], + ], $response->toArray()['_links']['items']); + } + + public function testListChecklistItemsAsChecklistSubresourceIsDeniedForUnrelatedUser() { + $checklist = static::getFixture('checklist1'); + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('GET', '/checklists/'.$checklist->getId().'/checklist_items') + ; + + $this->assertResponseStatusCodeSame(404); + } +} diff --git a/api/tests/Api/ChecklistItems/ReadChecklistItemTest.php b/api/tests/Api/ChecklistItems/ReadChecklistItemTest.php new file mode 100644 index 0000000000..9e362711a8 --- /dev/null +++ b/api/tests/Api/ChecklistItems/ReadChecklistItemTest.php @@ -0,0 +1,108 @@ +request('GET', '/checklist_items/'.$checklistItem->getId()); + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testGetSingleChecklistItemIsDeniedForUnrelatedUser() { + /** @var ChecklistItem $checklistItem */ + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('GET', '/checklist_items/'.$checklistItem->getId()) + ; + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testGetSingleChecklistItemIsDeniedForInactiveCollaborator() { + /** @var ChecklistItem $checklistItem */ + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('GET', '/checklist_items/'.$checklistItem->getId()) + ; + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testGetSingleChecklistItemIsAllowedForGuest() { + /** @var ChecklistItem $checklistItem */ + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials(['email' => static::$fixtures['user3guest']->getEmail()]) + ->request('GET', '/checklist_items/'.$checklistItem->getId()) + ; + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'id' => $checklistItem->getId(), + 'text' => $checklistItem->text, + '_links' => [ + 'checklist' => ['href' => $this->getIriFor('checklist1')], + ], + ]); + } + + public function testGetSingleChecklistItemIsAllowedForMember() { + /** @var ChecklistItem $checklistItem */ + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials(['email' => static::$fixtures['user2member']->getEmail()]) + ->request('GET', '/checklist_items/'.$checklistItem->getId()) + ; + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'id' => $checklistItem->getId(), + 'text' => $checklistItem->text, + '_links' => [ + 'checklist' => ['href' => $this->getIriFor('checklist1')], + ], + ]); + } + + public function testGetSingleChecklistItemIsAllowedForManager() { + /** @var ChecklistItem $checklistItem */ + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials()->request('GET', '/checklist_items/'.$checklistItem->getId()); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'id' => $checklistItem->getId(), + 'text' => $checklistItem->text, + '_links' => [ + 'checklist' => ['href' => $this->getIriFor('checklist1')], + ], + ]); + } + + public function testGetSingleChecklistItemFromCampPrototypeIsAllowedForUnrelatedUser() { + /** @var ChecklistItem $checklistItem */ + $checklistItem = static::getFixture('checklistItemPrototype_1_1'); + static::createClientWithCredentials()->request('GET', '/checklist_items/'.$checklistItem->getId()); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'id' => $checklistItem->getId(), + 'text' => $checklistItem->text, + '_links' => [ + 'checklist' => ['href' => $this->getIriFor('checklist1campPrototype')], + ], + ]); + } +} diff --git a/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php b/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php new file mode 100644 index 0000000000..ca6ceae23a --- /dev/null +++ b/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php @@ -0,0 +1,220 @@ +request('PATCH', '/checklist_items/'.$checklistItem->getId(), ['json' => [ + 'text' => 'Ziel 2', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + $this->assertResponseStatusCodeSame(401); + $this->assertJsonContains([ + 'code' => 401, + 'message' => 'JWT Token not found', + ]); + } + + public function testPatchChecklistItemIsDeniedForUnrelatedUser() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('PATCH', '/checklist_items/'.$checklistItem->getId(), ['json' => [ + 'text' => 'Ziel 2', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testPatchChecklistItemIsDeniedForInactiveCollaborator() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials(['email' => static::$fixtures['user5inactive']->getEmail()]) + ->request('PATCH', '/checklist_items/'.$checklistItem->getId(), ['json' => [ + 'text' => 'Ziel 2', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(404); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Not Found', + ]); + } + + public function testPatchChecklistItemIsDeniedForGuest() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials(['email' => static::$fixtures['user3guest']->getEmail()]) + ->request('PATCH', '/checklist_items/'.$checklistItem->getId(), ['json' => [ + 'text' => 'Ziel 2', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testPatchChecklistItemIsAllowedForMember() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + $response = static::createClientWithCredentials(['email' => static::$fixtures['user2member']->getEmail()]) + ->request('PATCH', '/checklist_items/'.$checklistItem->getId(), ['json' => [ + 'text' => 'Ziel 2', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'text' => 'Ziel 2', + ]); + } + + public function testPatchChecklistItemIsAllowedForManager() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + $response = static::createClientWithCredentials()->request('PATCH', '/checklist_items/'.$checklistItem->getId(), ['json' => [ + 'text' => 'Ziel 2', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'text' => 'Ziel 2', + ]); + } + + public function testPatchChecklistItemInCampPrototypeIsDeniedForUnrelatedUser() { + $checklistItem = static::getFixture('checklistItemPrototype_1_1'); + $response = static::createClientWithCredentials()->request('PATCH', '/checklist_items/'.$checklistItem->getId(), ['json' => [ + 'text' => 'Ziel 2', + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testPatchChecklistItemDisallowsChangingChecklist() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials()->request('PATCH', '/checklist_items/'.$checklistItem->getId(), ['json' => [ + 'checklist' => $this->getIriFor('checklistItem2_1_1'), + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains([ + 'detail' => 'Extra attributes are not allowed ("checklist" is unknown).', + ]); + } + + public function testPatchChecklistItemValidatesNullText() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials()->request( + 'PATCH', + '/checklist_items/'.$checklistItem->getId(), + [ + 'json' => [ + 'text' => null, + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ] + ); + + $this->assertResponseStatusCodeSame(400); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'The type of the "text" attribute must be "string", "NULL" given.', + ]); + } + + public function testPatchChecklistItemValidatesBlankText() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials()->request( + 'PATCH', + '/checklist_items/'.$checklistItem->getId(), + [ + 'json' => [ + 'text' => ' ', + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ] + ); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'text', + 'message' => 'This value should not be blank.', + ], + ], + ]); + } + + public function testPatchChecklistItemValidatesTooLongText() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials()->request( + 'PATCH', + '/checklist_items/'.$checklistItem->getId(), + [ + 'json' => [ + 'text' => str_repeat('l', 65), + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ] + ); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'violations' => [ + [ + 'propertyPath' => 'text', + 'message' => 'This value is too long. It should have 64 characters or less.', + ], + ], + ]); + } + + public function testPatchChecklistItemTrimsText() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials()->request( + 'PATCH', + '/checklist_items/'.$checklistItem->getId(), + [ + 'json' => [ + 'text' => " \t Ziel 2\t ", + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], ] + ); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains( + [ + 'text' => 'Ziel 2', + ] + ); + } + + public function testPatchChecklistItemCleansForbiddenCharactersFromText() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + $client = static::createClientWithCredentials(); + $client->disableReboot(); + $client->request( + 'PATCH', + '/checklist_items/'.$checklistItem->getId(), + [ + 'json' => [ + 'text' => "Ziel2\n\t", + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], ] + ); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains( + [ + 'text' => 'Ziel2', + ] + ); + } +} From cc876bbe46a93e97c1bd2634baceb108a5f050a7 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Sun, 16 Jun 2024 13:58:29 +0200 Subject: [PATCH 07/30] Add ChecklistNode --- api/fixtures/checklistNodes.yml | 15 + api/fixtures/contentTypes.yml | 4 + .../schema/Version20240616104153.php | 34 + .../schema/Version20240616143000.php | 34 + api/src/Entity/ChecklistItem.php | 8 + api/src/Entity/ContentNode/ChecklistNode.php | 108 ++ .../Repository/ChecklistNodeRepository.php | 20 + .../ChecklistNodePersistProcessor.php | 40 + .../ContentNode/ListContentNodesTest.php | 6 +- .../Api/ContentTypes/ListContentTypesTest.php | 8 +- .../SnapshotTests/EndpointPerformanceTest.php | 4 +- .../Api/SnapshotTests/ReadItemFixtureMap.php | 1 + ...Structure with data set activities__1.json | 6 +- ...ta set content_nodechecklist_nodes__1.json | 42 + ...ata set content_nodecolumn_layouts__1.json | 34 +- ...ucture with data set content_nodes__1.json | 50 +- ...ucture with data set content_types__1.json | 16 + ...ta set content_nodechecklist_nodes__1.json | 25 + ...ure with data set schedule_entries__1.json | 6 +- ...est__testOpenApiSpecMatchesSnapshot__1.yml | 1129 +++++++++++++++++ ...t__testRootEndpointMatchesSnapshot__1.json | 4 + e2e/specs/httpCache.cy.js | 2 +- .../responses/content_types_collection.json | 18 +- 23 files changed, 1579 insertions(+), 35 deletions(-) create mode 100644 api/fixtures/checklistNodes.yml create mode 100644 api/migrations/schema/Version20240616104153.php create mode 100644 api/migrations/schema/Version20240616143000.php create mode 100644 api/src/Entity/ContentNode/ChecklistNode.php create mode 100644 api/src/Repository/ChecklistNodeRepository.php create mode 100644 api/src/State/ContentNode/ChecklistNodePersistProcessor.php create mode 100644 api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json create mode 100644 api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set content_nodechecklist_nodes__1.json diff --git a/api/fixtures/checklistNodes.yml b/api/fixtures/checklistNodes.yml new file mode 100644 index 0000000000..9351448280 --- /dev/null +++ b/api/fixtures/checklistNodes.yml @@ -0,0 +1,15 @@ +App\Entity\ContentNode\ChecklistNode: + checklistNode3: + root: '@columnLayout3' + parent: '@columnLayout3' + slot: '1' + position: 1 + instanceName: + contentType: '@contentTypeChecklist' + checklistNodeCampUnrelated: + root: '@columnLayout1campUnrelated' + parent: '@columnLayout1campUnrelated' + slot: '1' + position: 5 + instanceName: + contentType: '@contentTypeChecklist' \ No newline at end of file diff --git a/api/fixtures/contentTypes.yml b/api/fixtures/contentTypes.yml index a620bf2ee9..32d094ec28 100644 --- a/api/fixtures/contentTypes.yml +++ b/api/fixtures/contentTypes.yml @@ -40,3 +40,7 @@ App\Entity\ContentType: active: true entityClass: 'App\Entity\ContentNode\MultiSelect' jsonConfig: { items: [ 'outdoorTechnique', 'security', 'natureAndEnvironment', 'pioneeringTechnique', 'campsiteAndSurroundings', 'preventionAndIntegration' ] } + contentTypeChecklist: + name: 'Checklist' + active: true + entityClass: 'App\Entity\ContentNode\ChecklistNode' diff --git a/api/migrations/schema/Version20240616104153.php b/api/migrations/schema/Version20240616104153.php new file mode 100644 index 0000000000..340d9d16fd --- /dev/null +++ b/api/migrations/schema/Version20240616104153.php @@ -0,0 +1,34 @@ +addSql('CREATE TABLE checklistnode_checklistitem (checklistnode_id VARCHAR(16) NOT NULL, checklistitem_id VARCHAR(16) NOT NULL, PRIMARY KEY(checklistnode_id, checklistitem_id))'); + $this->addSql('CREATE INDEX IDX_5A2B5B31DE6B6F00 ON checklistnode_checklistitem (checklistnode_id)'); + $this->addSql('CREATE INDEX IDX_5A2B5B318A09A289 ON checklistnode_checklistitem (checklistitem_id)'); + $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B31DE6B6F00 FOREIGN KEY (checklistnode_id) REFERENCES content_node (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B318A09A289 FOREIGN KEY (checklistitem_id) REFERENCES checklist_item (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT FK_5A2B5B31DE6B6F00'); + $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT FK_5A2B5B318A09A289'); + $this->addSql('DROP TABLE checklistnode_checklistitem'); + } +} diff --git a/api/migrations/schema/Version20240616143000.php b/api/migrations/schema/Version20240616143000.php new file mode 100644 index 0000000000..f18be0dcb8 --- /dev/null +++ b/api/migrations/schema/Version20240616143000.php @@ -0,0 +1,34 @@ +addSql(" + INSERT INTO public.content_type (id, name, active, entityclass, jsonconfig, createtime, updatetime) + VALUES ( + 'a4211c11211c', + 'Checklist', + true, + 'App\\Entity\\ContentNode\\ChecklistNode', + null, + '2024-06-16 14:30:00', + '2024-06-16 14:30:00' + ); + "); + } + + public function down(Schema $schema): void { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DELETE FROM public.content_type WHERE id IN (\'a4211c11211c\')'); + } +} diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php index aee087b9a6..c6c453806b 100644 --- a/api/src/Entity/ChecklistItem.php +++ b/api/src/Entity/ChecklistItem.php @@ -12,6 +12,7 @@ use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; +use App\Entity\ContentNode\ChecklistNode; use App\InputFilter; use App\Repository\ChecklistItemRepository; use App\Util\EntityMap; @@ -100,6 +101,12 @@ class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFr #[ORM\OneToMany(targetEntity: ChecklistItem::class, mappedBy: 'parent', cascade: ['persist'])] public Collection $children; + /** + * All ChecklistNodes that have selected this ChecklistItem. + */ + #[ORM\ManyToMany(targetEntity: ChecklistNode::class, mappedBy: 'checklistItems')] + public Collection $checklistNodes; + /** * The human readable text of the checklist-item. */ @@ -125,6 +132,7 @@ class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFr public function __construct() { parent::__construct(); $this->children = new ArrayCollection(); + $this->checklistNodes = new ArrayCollection(); } #[ApiProperty(readable: false)] diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php new file mode 100644 index 0000000000..237296a7b9 --- /dev/null +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -0,0 +1,108 @@ + ['write', 'update']], + security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)', + validationContext: ['groups' => ['Default', 'update']] + ), + new Delete( + security: '(is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)) and object.parent !== null' + ), + new GetCollection( + security: 'is_authenticated()' + ), + new Post( + processor: ChecklistNodePersistProcessor::class, + denormalizationContext: ['groups' => ['write', 'create']], + securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)', + validationContext: ['groups' => ['Default', 'create']], + ), + ], + denormalizationContext: ['groups' => ['write']], + normalizationContext: ['groups' => ['read']], + routePrefix: '/content_node' +)] +#[ORM\Entity(repositoryClass: ChecklistNodeRepository::class)] +class ChecklistNode extends ContentNode { + /** + * The content types that are most likely to be useful for planning programme of this category. + */ + #[ApiProperty(example: '["/checklist_items/1a2b3c4d"]')] + #[Groups(['read'])] + #[ORM\ManyToMany(targetEntity: ChecklistItem::class, inversedBy: 'checklistNodes')] + #[ORM\JoinTable(name: 'checklistnode_checklistitem')] + #[ORM\JoinColumn(name: 'checklistnode_id', referencedColumnName: 'id')] + #[ORM\InverseJoinColumn(name: 'checklistitem_id', referencedColumnName: 'id')] + #[ORM\OrderBy(['position' => 'ASC'])] + public Collection $checklistItems; + + #[ApiProperty(example: '["1a2b3c4d"]')] + #[Groups(['write'])] + public ?array $addChecklistItemIds = []; + + #[ApiProperty(example: '["1a2b3c4d"]')] + #[Groups(['write'])] + public ?array $removeChecklistItemIds = []; + + public function __construct() { + parent::__construct(); + $this->checklistItems = new ArrayCollection(); + } + + /** + * @return ChecklistItem[] + */ + public function getChecklistItems(): array { + return $this->checklistItems->getValues(); + } + + public function addChecklistItem(ChecklistItem $checklistItem) { + $this->checklistItems->add($checklistItem); + } + + public function removeChecklistItem(ChecklistItem $checklistItem) { + $this->checklistItems->removeElement($checklistItem); + } + + /** + * @param ChecklistNode $prototype + * @param EntityMap $entityMap + */ + public function copyFromPrototype($prototype, $entityMap): void { + parent::copyFromPrototype($prototype, $entityMap); + + // copy all checklist-items + foreach ($prototype->checklistItems as $itemPrototype) { + /** @var ChecklistItem $itemPrototype */ + /** @var ChecklistItem $checklilstItem */ + $checklilstItem = $entityMap->get($itemPrototype); + $this->addChecklistItem($checklilstItem); + } + } +} diff --git a/api/src/Repository/ChecklistNodeRepository.php b/api/src/Repository/ChecklistNodeRepository.php new file mode 100644 index 0000000000..c6b9bafe84 --- /dev/null +++ b/api/src/Repository/ChecklistNodeRepository.php @@ -0,0 +1,20 @@ + + */ +class ChecklistNodeRepository extends ContentNodeRepository { + public function __construct(EntityManagerInterface $em) { + parent::__construct($em, ChecklistNode::class); + } +} diff --git a/api/src/State/ContentNode/ChecklistNodePersistProcessor.php b/api/src/State/ContentNode/ChecklistNodePersistProcessor.php new file mode 100644 index 0000000000..b138cf7b73 --- /dev/null +++ b/api/src/State/ContentNode/ChecklistNodePersistProcessor.php @@ -0,0 +1,40 @@ + + */ +class ChecklistNodePersistProcessor extends ContentNodePersistProcessor { + public function __construct( + ProcessorInterface $decorated, + private ChecklistItemRepository $checklistItemRepository, + ) { + parent::__construct($decorated); + } + + public function onBefore($data, Operation $operation, array $uriVariables = [], array $context = []): ChecklistNode { + /** @var ChecklistNode $data */ + $data = parent::onBefore($data, $operation, $uriVariables, $context); + + if (null !== $data->addChecklistItemIds) { + foreach ($data->addChecklistItemIds as $checklistItemId) { + $checklistItem = $this->checklistItemRepository->find($checklistItemId); + $data->addChecklistItem($checklistItem); + } + } + if (null !== $data->removeChecklistItemIds) { + foreach ($data->removeChecklistItemIds as $checklistItemId) { + $checklistItem = $this->checklistItemRepository->find($checklistItemId); + $data->removeChecklistItem($checklistItem); + } + } + + return $data; + } +} diff --git a/api/tests/Api/ContentNodes/ContentNode/ListContentNodesTest.php b/api/tests/Api/ContentNodes/ContentNode/ListContentNodesTest.php index 3ea968a731..430a93b593 100644 --- a/api/tests/Api/ContentNodes/ContentNode/ListContentNodesTest.php +++ b/api/tests/Api/ContentNodes/ContentNode/ListContentNodesTest.php @@ -23,7 +23,7 @@ public function testListContentNodesIsAllowedForLoggedInUserButFiltered() { $response = static::createClientWithCredentials()->request('GET', '/content_nodes'); $this->assertResponseStatusCodeSame(200); $this->assertJsonContains([ - 'totalItems' => 21, + 'totalItems' => 22, '_links' => [ 'items' => [], ], @@ -37,6 +37,7 @@ public function testListContentNodesIsAllowedForLoggedInUserButFiltered() { ['href' => $this->getIriFor('columnLayoutChild1')], ['href' => $this->getIriFor('columnLayout2Child1')], ['href' => $this->getIriFor('columnLayout3')], + ['href' => $this->getIriFor('checklistNode3')], ['href' => $this->getIriFor('columnLayout4')], ['href' => $this->getIriFor('columnLayout5')], ['href' => $this->getIriFor('columnLayout1camp2')], @@ -61,7 +62,7 @@ public function testListContentNodesFilteredByPeriodIsAllowedForCollaborator() { $response = static::createClientWithCredentials()->request('GET', '/content_nodes?period=%2Fperiods%2F'.$period->getId()); $this->assertResponseStatusCodeSame(200); $this->assertJsonContains([ - 'totalItems' => 12, + 'totalItems' => 13, '_links' => [ 'items' => [], ], @@ -73,6 +74,7 @@ public function testListContentNodesFilteredByPeriodIsAllowedForCollaborator() { ['href' => $this->getIriFor('columnLayout1')], ['href' => $this->getIriFor('columnLayoutChild1')], ['href' => $this->getIriFor('columnLayout3')], + ['href' => $this->getIriFor('checklistNode3')], ['href' => $this->getIriFor('singleText1')], ['href' => $this->getIriFor('singleText2')], ['href' => $this->getIriFor('safetyConcept1')], diff --git a/api/tests/Api/ContentTypes/ListContentTypesTest.php b/api/tests/Api/ContentTypes/ListContentTypesTest.php index 79cd0bf6ed..c5a58ccee6 100644 --- a/api/tests/Api/ContentTypes/ListContentTypesTest.php +++ b/api/tests/Api/ContentTypes/ListContentTypesTest.php @@ -12,7 +12,7 @@ public function testListContentTypesIsAllowedForAnonymousUser() { $response = static::createBasicClient()->request('GET', '/content_types'); $this->assertResponseStatusCodeSame(200); $this->assertJsonContains([ - 'totalItems' => 10, + 'totalItems' => 11, '_links' => [ 'items' => [], ], @@ -21,14 +21,14 @@ public function testListContentTypesIsAllowedForAnonymousUser() { ], ]); - $this->assertCount(10, $response->toArray()['_links']['items']); + $this->assertCount(11, $response->toArray()['_links']['items']); } public function testListContentTypesIsAllowedForLoggedInUser() { $response = static::createClientWithCredentials()->request('GET', '/content_types'); $this->assertResponseStatusCodeSame(200); $this->assertJsonContains([ - 'totalItems' => 10, + 'totalItems' => 11, '_links' => [ 'items' => [], ], @@ -36,6 +36,6 @@ public function testListContentTypesIsAllowedForLoggedInUser() { 'items' => [], ], ]); - $this->assertCount(10, $response->toArray()['_links']['items']); + $this->assertCount(11, $response->toArray()['_links']['items']); } } diff --git a/api/tests/Api/SnapshotTests/EndpointPerformanceTest.php b/api/tests/Api/SnapshotTests/EndpointPerformanceTest.php index 93cb47f721..1463bb5cd4 100644 --- a/api/tests/Api/SnapshotTests/EndpointPerformanceTest.php +++ b/api/tests/Api/SnapshotTests/EndpointPerformanceTest.php @@ -186,9 +186,11 @@ protected function getSnapshotId(): string { private static function getContentNodeEndpointQueryCountRanges(): array { return [ - '/content_nodes' => [8, 9], + '/content_nodes' => [8, 10], '/content_node/column_layouts' => [6, 6], '/content_node/column_layouts/item' => [10, 10], + '/content_node/checklist_nodes' => [6, 7], + '/content_node/checklist_nodes/item' => [9, 9], '/content_node/material_nodes' => [6, 7], '/content_node/material_nodes/item' => [9, 9], '/content_node/multi_selects' => [6, 7], diff --git a/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php b/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php index a734aa4518..33fa16dfd3 100644 --- a/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php +++ b/api/tests/Api/SnapshotTests/ReadItemFixtureMap.php @@ -13,6 +13,7 @@ public static function get(string $collectionEndpoint, array $fixtures): mixed { '/categories' => $fixtures['category1'], '/checklists' => $fixtures['checklist1'], '/checklist_items' => $fixtures['checklistItem1_1_1'], + '/content_node/checklist_nodes' => $fixtures['checklistNode3'], '/content_node/column_layouts' => $fixtures['columnLayout2'], '/content_node/responsive_layouts' => $fixtures['responsiveLayout1'], '/content_types' => $fixtures['contentTypeSafetyConcept'], diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set activities__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set activities__1.json index eab677ee6d..17f0da395c 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set activities__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set activities__1.json @@ -381,7 +381,11 @@ }, "rootContentNode": { "_links": { - "children": [], + "children": [ + { + "href": "escaped_value" + } + ], "contentType": { "href": "escaped_value" }, diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json new file mode 100644 index 0000000000..a657327a12 --- /dev/null +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json @@ -0,0 +1,42 @@ +{ + "_embedded": { + "items": [ + { + "_links": { + "checklist": "escaped_value", + "checklistItems": [], + "children": [], + "contentType": { + "href": "escaped_value" + }, + "parent": { + "href": "escaped_value" + }, + "root": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "contentTypeName": "escaped_value", + "data": "escaped_value", + "id": "escaped_value", + "instanceName": "escaped_value", + "position": "escaped_value", + "slot": "escaped_value" + } + ] + }, + "_links": { + "items": [ + { + "href": "escaped_value" + } + ], + "self": { + "href": "escaped_value" + } + }, + "totalItems": "escaped_value" +} diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodecolumn_layouts__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodecolumn_layouts__1.json index dbdfe83685..2c6362ca64 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodecolumn_layouts__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodecolumn_layouts__1.json @@ -147,7 +147,9 @@ "contentType": { "href": "escaped_value" }, - "parent": "escaped_value", + "parent": { + "href": "escaped_value" + }, "root": { "href": "escaped_value" }, @@ -171,7 +173,17 @@ }, { "_links": { - "children": [], + "children": [ + { + "href": "escaped_value" + }, + { + "href": "escaped_value" + }, + { + "href": "escaped_value" + } + ], "contentType": { "href": "escaped_value" }, @@ -188,6 +200,10 @@ "contentTypeName": "escaped_value", "data": { "columns": [ + { + "slot": "escaped_value", + "width": "escaped_value" + }, { "slot": "escaped_value", "width": "escaped_value" @@ -202,12 +218,6 @@ { "_links": { "children": [ - { - "href": "escaped_value" - }, - { - "href": "escaped_value" - }, { "href": "escaped_value" } @@ -215,9 +225,7 @@ "contentType": { "href": "escaped_value" }, - "parent": { - "href": "escaped_value" - }, + "parent": "escaped_value", "root": { "href": "escaped_value" }, @@ -228,10 +236,6 @@ "contentTypeName": "escaped_value", "data": { "columns": [ - { - "slot": "escaped_value", - "width": "escaped_value" - }, { "slot": "escaped_value", "width": "escaped_value" diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json index 6945297e6c..540f43263f 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json @@ -80,11 +80,15 @@ }, { "_links": { + "checklist": "escaped_value", + "checklistItems": [], "children": [], "contentType": { "href": "escaped_value" }, - "parent": "escaped_value", + "parent": { + "href": "escaped_value" + }, "root": { "href": "escaped_value" }, @@ -93,14 +97,7 @@ } }, "contentTypeName": "escaped_value", - "data": { - "columns": [ - { - "slot": "escaped_value", - "width": "escaped_value" - } - ] - }, + "data": "escaped_value", "id": "escaped_value", "instanceName": "escaped_value", "position": "escaped_value", @@ -644,6 +641,38 @@ "position": "escaped_value", "slot": "escaped_value" }, + { + "_links": { + "children": [ + { + "href": "escaped_value" + } + ], + "contentType": { + "href": "escaped_value" + }, + "parent": "escaped_value", + "root": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "contentTypeName": "escaped_value", + "data": { + "columns": [ + { + "slot": "escaped_value", + "width": "escaped_value" + } + ] + }, + "id": "escaped_value", + "instanceName": "escaped_value", + "position": "escaped_value", + "slot": "escaped_value" + }, { "_links": { "children": [ @@ -740,6 +769,9 @@ { "href": "escaped_value" }, + { + "href": "escaped_value" + }, { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_types__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_types__1.json index d8875812f3..8656a040cf 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_types__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_types__1.json @@ -118,6 +118,19 @@ "id": "escaped_value", "name": "escaped_value" }, + { + "_links": { + "contentNodes": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "active": "escaped_value", + "id": "escaped_value", + "name": "escaped_value" + }, { "_links": { "contentNodes": { @@ -162,6 +175,9 @@ { "href": "escaped_value" }, + { + "href": "escaped_value" + }, { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set content_nodechecklist_nodes__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set content_nodechecklist_nodes__1.json new file mode 100644 index 0000000000..f883ff8e4f --- /dev/null +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set content_nodechecklist_nodes__1.json @@ -0,0 +1,25 @@ +{ + "_links": { + "checklist": "escaped_value", + "checklistItems": [], + "children": [], + "contentType": { + "href": "escaped_value" + }, + "parent": { + "href": "escaped_value" + }, + "root": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "contentTypeName": "escaped_value", + "data": "escaped_value", + "id": "escaped_value", + "instanceName": "escaped_value", + "position": "escaped_value", + "slot": "escaped_value" +} diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set schedule_entries__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set schedule_entries__1.json index 3bc182b966..56715cb3b4 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set schedule_entries__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set schedule_entries__1.json @@ -4,7 +4,11 @@ "_embedded": { "rootContentNode": { "_links": { - "children": [], + "children": [ + { + "href": "escaped_value" + } + ], "contentType": { "href": "escaped_value" }, diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 70dbc0ebc5..67a199a93c 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -8311,6 +8311,846 @@ components: - position - text type: object + ChecklistNode-read: + deprecated: false + description: '' + properties: + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + checklistItems: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + description: 'Holds the actual data of the content node.' + example: + text: 'dummy text' + items: + type: string + type: + - array + - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - checklistItems + - children + - contentType + - position + type: object + ChecklistNode-write_create: + deprecated: false + description: '' + properties: + addChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + checklistItems: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + data: + description: 'Holds the actual data of the content node.' + example: + text: 'dummy text' + items: + type: string + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + removeChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - checklistItems + - contentType + - parent + - position + type: object + ChecklistNode-write_update: + deprecated: false + description: '' + properties: + addChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + checklistItems: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + data: + description: 'Holds the actual data of the content node.' + example: + text: 'dummy text' + items: + type: string + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + removeChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - checklistItems + - position + type: object + ChecklistNode.jsonapi: + deprecated: false + description: '' + properties: + addChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + writeOnly: true + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + checklistItems: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + readOnly: true + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + description: 'Holds the actual data of the content node.' + example: + text: 'dummy text' + items: + type: string + type: + - array + - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + removeChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + writeOnly: true + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - checklistItems + - children + - contentType + - position + type: object + ChecklistNode.jsonhal-read: + deprecated: false + description: '' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + checklistItems: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + description: 'Holds the actual data of the content node.' + example: + text: 'dummy text' + items: + type: string + type: + - array + - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - checklistItems + - children + - contentType + - position + type: object + ChecklistNode.jsonhal-write_create: + deprecated: false + description: '' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + addChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + checklistItems: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + data: + description: 'Holds the actual data of the content node.' + example: + text: 'dummy text' + items: + type: string + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + removeChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - checklistItems + - contentType + - parent + - position + type: object + ChecklistNode.jsonld-read: + deprecated: false + description: '' + properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + checklistItems: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + description: 'Holds the actual data of the content node.' + example: + text: 'dummy text' + items: + type: string + type: + - array + - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - checklistItems + - children + - contentType + - position + type: object + ChecklistNode.jsonld-write_create: + deprecated: false + description: '' + properties: + addChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + checklist: + description: 'The Checklist this Item belongs to.' + example: /checklists/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + checklistItems: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + data: + description: 'Holds the actual data of the content node.' + example: + text: 'dummy text' + items: + type: string + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + removeChecklistItemIds: + example: '["1a2b3c4d"]' + items: + type: string + type: + - array + - 'null' + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - checklistItems + - contentType + - parent + - position + type: object ColumnLayout-read: deprecated: false description: '' @@ -24613,6 +25453,295 @@ paths: summary: 'Updates the Checklist resource.' tags: - Checklist + /content_node/checklist_nodes: + get: + deprecated: false + description: 'Retrieves the collection of ChecklistNode resources.' + operationId: api_content_nodechecklist_nodes_get_collection + parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: contentType + required: false + schema: + type: string + style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: period + required: false + schema: + type: string + style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: root + required: false + schema: + type: string + style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: true + in: query + name: 'contentType[]' + required: false + schema: + items: + type: string + type: array + style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: true + in: query + name: 'root[]' + required: false + schema: + items: + type: string + type: array + style: form + responses: + 200: + content: + application/hal+json: + schema: + properties: + _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/ChecklistNode.jsonhal-read' }, type: array } }, type: object }, { type: object }] } + _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } + itemsPerPage: { minimum: 0, type: integer } + totalItems: { minimum: 0, type: integer } + required: + - _embedded + - _links + type: object + application/json: + schema: + items: + $ref: '#/components/schemas/ChecklistNode-read' + type: array + application/ld+json: + schema: + properties: + 'hydra:member': { items: { $ref: '#/components/schemas/ChecklistNode.jsonld-read' }, type: array } + 'hydra:search': { properties: { '@type': { type: string }, 'hydra:mapping': { items: { properties: { '@type': { type: string }, property: { type: ['null', string] }, required: { type: boolean }, variable: { type: string } }, type: object }, type: array }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string } }, type: object } + 'hydra:totalItems': { minimum: 0, type: integer } + 'hydra:view': { example: { '@id': string, 'hydra:first': string, 'hydra:last': string, 'hydra:next': string, 'hydra:previous': string, type: string }, properties: { '@id': { format: iri-reference, type: string }, '@type': { type: string }, 'hydra:first': { format: iri-reference, type: string }, 'hydra:last': { format: iri-reference, type: string }, 'hydra:next': { format: iri-reference, type: string }, 'hydra:previous': { format: iri-reference, type: string } }, type: object } + required: + - 'hydra:member' + type: object + application/vnd.api+json: + schema: + items: + $ref: '#/components/schemas/ChecklistNode.jsonapi' + type: array + text/html: + schema: + items: + $ref: '#/components/schemas/ChecklistNode-read' + type: array + description: 'ChecklistNode collection' + summary: 'Retrieves the collection of ChecklistNode resources.' + tags: + - ChecklistNode + parameters: [] + post: + deprecated: false + description: 'Creates a ChecklistNode resource.' + operationId: api_content_nodechecklist_nodes_post + parameters: [] + requestBody: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonhal-write_create' + application/json: + schema: + $ref: '#/components/schemas/ChecklistNode-write_create' + application/ld+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonld-write_create' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/ChecklistNode-write_create' + description: 'The new ChecklistNode resource' + required: true + responses: + 201: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonhal-read' + application/json: + schema: + $ref: '#/components/schemas/ChecklistNode-read' + application/ld+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonld-read' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/ChecklistNode-read' + description: 'ChecklistNode resource created' + links: [] + 400: + description: 'Invalid input' + 422: + description: 'Unprocessable entity' + summary: 'Creates a ChecklistNode resource.' + tags: + - ChecklistNode + '/content_node/checklist_nodes/{id}': + delete: + deprecated: false + description: 'Removes the ChecklistNode resource.' + operationId: api_content_nodechecklist_nodes_id_delete + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'ChecklistNode identifier' + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + 204: + description: 'ChecklistNode resource deleted' + 404: + description: 'Resource not found' + summary: 'Removes the ChecklistNode resource.' + tags: + - ChecklistNode + get: + deprecated: false + description: 'Retrieves a ChecklistNode resource.' + operationId: api_content_nodechecklist_nodes_id_get + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'ChecklistNode identifier' + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + 200: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonhal-read' + application/json: + schema: + $ref: '#/components/schemas/ChecklistNode-read' + application/ld+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonld-read' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/ChecklistNode-read' + description: 'ChecklistNode resource' + 404: + description: 'Resource not found' + summary: 'Retrieves a ChecklistNode resource.' + tags: + - ChecklistNode + parameters: [] + patch: + deprecated: false + description: 'Updates the ChecklistNode resource.' + operationId: api_content_nodechecklist_nodes_id_patch + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'ChecklistNode identifier' + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/merge-patch+json: + schema: + $ref: '#/components/schemas/ChecklistNode-write_update' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonapi' + description: 'The updated ChecklistNode resource' + required: true + responses: + 200: + content: + application/hal+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonhal-read' + application/json: + schema: + $ref: '#/components/schemas/ChecklistNode-read' + application/ld+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonld-read' + application/vnd.api+json: + schema: + $ref: '#/components/schemas/ChecklistNode.jsonapi' + text/html: + schema: + $ref: '#/components/schemas/ChecklistNode-read' + description: 'ChecklistNode resource updated' + links: [] + 400: + description: 'Invalid input' + 404: + description: 'Resource not found' + 422: + description: 'Unprocessable entity' + summary: 'Updates the ChecklistNode resource.' + tags: + - ChecklistNode /content_node/column_layouts: get: deprecated: false diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json index 08bdba78ce..040d83054a 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json @@ -28,6 +28,10 @@ "href": "\/checklist_items{\/id}{?checklist,checklist[]}", "templated": true }, + "checklistNodes": { + "href": "\/content_node\/checklist_nodes{\/id}{?contentType,contentType[],root,root[],period}", + "templated": true + }, "checklists": { "href": "\/checklists{\/id}{?camp,camp[]}", "templated": true diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index 4b0fc346be..14a5412ee5 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -9,7 +9,7 @@ describe('HTTP cache tests', () => { cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { const headers = response.headers expect(headers.xkey).to.eq( - 'c462edd869f3 5e2028c55ee4 a4211c112939 f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' + 'a4211c11211c c462edd869f3 5e2028c55ee4 a4211c112939 f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' ) expect(headers['x-cache']).to.eq('MISS') cy.readFile('./specs/responses/content_types_collection.json').then((data) => diff --git a/e2e/specs/responses/content_types_collection.json b/e2e/specs/responses/content_types_collection.json index f209e6a3b5..54ec47fb5c 100644 --- a/e2e/specs/responses/content_types_collection.json +++ b/e2e/specs/responses/content_types_collection.json @@ -4,6 +4,9 @@ "href": "/api/content_types.jsonhal" }, "items": [ + { + "href": "/api/content_types/a4211c11211c" + }, { "href": "/api/content_types/c462edd869f3" }, @@ -36,9 +39,22 @@ } ] }, - "totalItems": 10, + "totalItems": 11, "_embedded": { "items": [ + { + "_links": { + "self": { + "href": "/api/content_types/a4211c11211c" + }, + "contentNodes": { + "href": "/api/content_node/checklist_nodes?contentType=%2Fapi%2Fcontent_types%2Fa4211c11211c" + } + }, + "name": "Checklist", + "active": true, + "id": "a4211c11211c" + }, { "_links": { "self": { From 93ea8748c44f0e67c4e690fdf85952836ac9c79e Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Tue, 18 Jun 2024 20:58:35 +0200 Subject: [PATCH 08/30] Add UnitTests fot checklistNode --- api/fixtures/checklistNodes.yml | 7 ++ api/tests/Api/Activities/ReadActivityTest.php | 2 +- .../ChecklistNode/CreateChecklistNodeTest.php | 35 +++++++ .../ChecklistNode/DeleteChecklistNodeTest.php | 17 ++++ .../ChecklistNode/ListChecklistNodeTest.php | 25 +++++ .../ChecklistNode/ReadChecklistNodeTest.php | 17 ++++ .../ChecklistNode/UpdateChecklistNodeTest.php | 17 ++++ .../ContentNode/ListContentNodesTest.php | 6 +- .../CreateContentNodeTestCase.php | 3 +- .../CreateRootColumnLayoutTest.php | 2 +- .../SnapshotTests/EndpointPerformanceTest.php | 2 +- ...Structure with data set activities__1.json | 3 + ...ta set content_nodechecklist_nodes__1.json | 28 +++++- ...ata set content_nodecolumn_layouts__1.json | 3 + ...ucture with data set content_nodes__1.json | 31 ++++++- ...Structure with data set activities__1.json | 30 ++++++ ...ta set content_nodechecklist_nodes__1.json | 1 - ...est__testOpenApiSpecMatchesSnapshot__1.yml | 93 +------------------ ...manceDidNotChangeForStableEndpoints__1.yml | 2 +- 19 files changed, 221 insertions(+), 103 deletions(-) create mode 100644 api/tests/Api/ContentNodes/ChecklistNode/CreateChecklistNodeTest.php create mode 100644 api/tests/Api/ContentNodes/ChecklistNode/DeleteChecklistNodeTest.php create mode 100644 api/tests/Api/ContentNodes/ChecklistNode/ListChecklistNodeTest.php create mode 100644 api/tests/Api/ContentNodes/ChecklistNode/ReadChecklistNodeTest.php create mode 100644 api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php diff --git a/api/fixtures/checklistNodes.yml b/api/fixtures/checklistNodes.yml index 9351448280..eb7ffe74cc 100644 --- a/api/fixtures/checklistNodes.yml +++ b/api/fixtures/checklistNodes.yml @@ -1,4 +1,11 @@ App\Entity\ContentNode\ChecklistNode: + checklistNode1: + root: '@columnLayout1' + parent: '@columnLayout1' + slot: '1' + position: 1 + instanceName: + contentType: '@contentTypeChecklist' checklistNode3: root: '@columnLayout3' parent: '@columnLayout3' diff --git a/api/tests/Api/Activities/ReadActivityTest.php b/api/tests/Api/Activities/ReadActivityTest.php index 2bccea6aa6..61c7394e24 100644 --- a/api/tests/Api/Activities/ReadActivityTest.php +++ b/api/tests/Api/Activities/ReadActivityTest.php @@ -93,7 +93,7 @@ public function testGetSingleActivityIsAllowedForMember() { $this->assertEquals($this->getIriFor($activity->getRootContentNode()), $data['_embedded']['rootContentNode']['_links']['self']['href']); $this->assertEquals($this->getIriFor($activity->getRootContentNode()), $data['_embedded']['rootContentNode']['_links']['root']['href']); $this->assertContains(['href' => $this->getIriFor('responsiveLayout1')], $data['_embedded']['rootContentNode']['_links']['children']); - $this->assertEquals(11, count($data['_embedded']['contentNodes'])); + $this->assertEquals(12, count($data['_embedded']['contentNodes'])); } public function testGetSingleActivityIsAllowedForManager() { diff --git a/api/tests/Api/ContentNodes/ChecklistNode/CreateChecklistNodeTest.php b/api/tests/Api/ContentNodes/ChecklistNode/CreateChecklistNodeTest.php new file mode 100644 index 0000000000..703e19bbe8 --- /dev/null +++ b/api/tests/Api/ContentNodes/ChecklistNode/CreateChecklistNodeTest.php @@ -0,0 +1,35 @@ +endpoint = '/content_node/checklist_nodes'; + $this->entityClass = ChecklistNode::class; + $this->defaultContentType = static::getFixture('contentTypeChecklist'); + } + + /** + * payload set up. + */ + public function getExampleWritePayload($attributes = [], $except = []) { + return parent::getExampleWritePayload( + array_merge( + [ + 'addChecklistItemIds' => null, + 'removeChecklistItemIds' => null, + ], + $attributes + ), + $except + ); + } +} diff --git a/api/tests/Api/ContentNodes/ChecklistNode/DeleteChecklistNodeTest.php b/api/tests/Api/ContentNodes/ChecklistNode/DeleteChecklistNodeTest.php new file mode 100644 index 0000000000..963a52a82b --- /dev/null +++ b/api/tests/Api/ContentNodes/ChecklistNode/DeleteChecklistNodeTest.php @@ -0,0 +1,17 @@ +endpoint = '/content_node/checklist_nodes'; + $this->defaultEntity = static::getFixture('checklistNode3'); + } +} diff --git a/api/tests/Api/ContentNodes/ChecklistNode/ListChecklistNodeTest.php b/api/tests/Api/ContentNodes/ChecklistNode/ListChecklistNodeTest.php new file mode 100644 index 0000000000..723700944e --- /dev/null +++ b/api/tests/Api/ContentNodes/ChecklistNode/ListChecklistNodeTest.php @@ -0,0 +1,25 @@ +endpoint = '/content_node/checklist_nodes'; + + $this->contentNodesCamp1and2 = [ + $this->getIriFor('checklistNode1'), + $this->getIriFor('checklistNode3'), + ]; + + $this->contentNodesCampUnrelated = [ + $this->getIriFor('checklistNodeCampUnrelated'), + ]; + } +} diff --git a/api/tests/Api/ContentNodes/ChecklistNode/ReadChecklistNodeTest.php b/api/tests/Api/ContentNodes/ChecklistNode/ReadChecklistNodeTest.php new file mode 100644 index 0000000000..f68349061a --- /dev/null +++ b/api/tests/Api/ContentNodes/ChecklistNode/ReadChecklistNodeTest.php @@ -0,0 +1,17 @@ +endpoint = '/content_node/checklist_nodes'; + $this->defaultEntity = static::getFixture('checklistNode3'); + } +} diff --git a/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php b/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php new file mode 100644 index 0000000000..182a81d0d5 --- /dev/null +++ b/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php @@ -0,0 +1,17 @@ +endpoint = '/content_node/checklist_nodes'; + $this->defaultEntity = static::getFixture('checklistNode1'); + } +} diff --git a/api/tests/Api/ContentNodes/ContentNode/ListContentNodesTest.php b/api/tests/Api/ContentNodes/ContentNode/ListContentNodesTest.php index 430a93b593..e8ad29383c 100644 --- a/api/tests/Api/ContentNodes/ContentNode/ListContentNodesTest.php +++ b/api/tests/Api/ContentNodes/ContentNode/ListContentNodesTest.php @@ -23,7 +23,7 @@ public function testListContentNodesIsAllowedForLoggedInUserButFiltered() { $response = static::createClientWithCredentials()->request('GET', '/content_nodes'); $this->assertResponseStatusCodeSame(200); $this->assertJsonContains([ - 'totalItems' => 22, + 'totalItems' => 23, '_links' => [ 'items' => [], ], @@ -33,6 +33,7 @@ public function testListContentNodesIsAllowedForLoggedInUserButFiltered() { ]); $this->assertEqualsCanonicalizing([ ['href' => $this->getIriFor('columnLayout1')], + ['href' => $this->getIriFor('checklistNode1')], ['href' => $this->getIriFor('columnLayout2')], ['href' => $this->getIriFor('columnLayoutChild1')], ['href' => $this->getIriFor('columnLayout2Child1')], @@ -62,7 +63,7 @@ public function testListContentNodesFilteredByPeriodIsAllowedForCollaborator() { $response = static::createClientWithCredentials()->request('GET', '/content_nodes?period=%2Fperiods%2F'.$period->getId()); $this->assertResponseStatusCodeSame(200); $this->assertJsonContains([ - 'totalItems' => 13, + 'totalItems' => 14, '_links' => [ 'items' => [], ], @@ -72,6 +73,7 @@ public function testListContentNodesFilteredByPeriodIsAllowedForCollaborator() { ]); $this->assertEqualsCanonicalizing([ ['href' => $this->getIriFor('columnLayout1')], + ['href' => $this->getIriFor('checklistNode1')], ['href' => $this->getIriFor('columnLayoutChild1')], ['href' => $this->getIriFor('columnLayout3')], ['href' => $this->getIriFor('checklistNode3')], diff --git a/api/tests/Api/ContentNodes/CreateContentNodeTestCase.php b/api/tests/Api/ContentNodes/CreateContentNodeTestCase.php index 8fdaa2f327..90c1ba47a8 100644 --- a/api/tests/Api/ContentNodes/CreateContentNodeTestCase.php +++ b/api/tests/Api/ContentNodes/CreateContentNodeTestCase.php @@ -179,11 +179,10 @@ public function testCreatePutsContentNodeAtEndOfSlot() { ], ['position'] )); - $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ 'slot' => '1', - 'position' => 1, + 'position' => 2, ]); } diff --git a/api/tests/Api/ContentNodes/RootColumnLayout/CreateRootColumnLayoutTest.php b/api/tests/Api/ContentNodes/RootColumnLayout/CreateRootColumnLayoutTest.php index eefa5b64dc..8baf213391 100644 --- a/api/tests/Api/ContentNodes/RootColumnLayout/CreateRootColumnLayoutTest.php +++ b/api/tests/Api/ContentNodes/RootColumnLayout/CreateRootColumnLayoutTest.php @@ -46,7 +46,7 @@ public function testCreateColumnLayoutAllowsMissingPosition() { static::createClientWithCredentials()->request('POST', $this->endpoint, ['json' => $this->getExampleWritePayload([], ['position'])]); $this->assertResponseStatusCodeSame(201); - $this->assertJsonContains(['position' => 1]); + $this->assertJsonContains(['position' => 2]); } public function testCreateColumnLayoutAllowsMissingInstanceName() { diff --git a/api/tests/Api/SnapshotTests/EndpointPerformanceTest.php b/api/tests/Api/SnapshotTests/EndpointPerformanceTest.php index 1463bb5cd4..46e474c2d9 100644 --- a/api/tests/Api/SnapshotTests/EndpointPerformanceTest.php +++ b/api/tests/Api/SnapshotTests/EndpointPerformanceTest.php @@ -186,7 +186,7 @@ protected function getSnapshotId(): string { private static function getContentNodeEndpointQueryCountRanges(): array { return [ - '/content_nodes' => [8, 10], + '/content_nodes' => [8, 11], '/content_node/column_layouts' => [6, 6], '/content_node/column_layouts/item' => [10, 10], '/content_node/checklist_nodes' => [6, 7], diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set activities__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set activities__1.json index 17f0da395c..47bf622d83 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set activities__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set activities__1.json @@ -240,6 +240,9 @@ "rootContentNode": { "_links": { "children": [ + { + "href": "escaped_value" + }, { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json index a657327a12..d92c7c8ab7 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json @@ -3,7 +3,30 @@ "items": [ { "_links": { - "checklist": "escaped_value", + "checklistItems": [], + "children": [], + "contentType": { + "href": "escaped_value" + }, + "parent": { + "href": "escaped_value" + }, + "root": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "contentTypeName": "escaped_value", + "data": "escaped_value", + "id": "escaped_value", + "instanceName": "escaped_value", + "position": "escaped_value", + "slot": "escaped_value" + }, + { + "_links": { "checklistItems": [], "children": [], "contentType": { @@ -30,6 +53,9 @@ }, "_links": { "items": [ + { + "href": "escaped_value" + }, { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodecolumn_layouts__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodecolumn_layouts__1.json index 2c6362ca64..9e9859e619 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodecolumn_layouts__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodecolumn_layouts__1.json @@ -218,6 +218,9 @@ { "_links": { "children": [ + { + "href": "escaped_value" + }, { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json index 540f43263f..87fb09384c 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json @@ -80,7 +80,30 @@ }, { "_links": { - "checklist": "escaped_value", + "checklistItems": [], + "children": [], + "contentType": { + "href": "escaped_value" + }, + "parent": { + "href": "escaped_value" + }, + "root": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "contentTypeName": "escaped_value", + "data": "escaped_value", + "id": "escaped_value", + "instanceName": "escaped_value", + "position": "escaped_value", + "slot": "escaped_value" + }, + { + "_links": { "checklistItems": [], "children": [], "contentType": { @@ -580,6 +603,9 @@ { "_links": { "children": [ + { + "href": "escaped_value" + }, { "href": "escaped_value" } @@ -772,6 +798,9 @@ { "href": "escaped_value" }, + { + "href": "escaped_value" + }, { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set activities__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set activities__1.json index 9b8c7684dd..81d49812a7 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set activities__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set activities__1.json @@ -123,6 +123,30 @@ "position": "escaped_value", "slot": "escaped_value" }, + { + "_links": { + "checklistItems": [], + "children": [], + "contentType": { + "href": "escaped_value" + }, + "parent": { + "href": "escaped_value" + }, + "root": { + "href": "escaped_value" + }, + "self": { + "href": "escaped_value" + } + }, + "contentTypeName": "escaped_value", + "data": "escaped_value", + "id": "escaped_value", + "instanceName": "escaped_value", + "position": "escaped_value", + "slot": "escaped_value" + }, { "_links": { "children": [], @@ -430,6 +454,9 @@ { "_links": { "children": [ + { + "href": "escaped_value" + }, { "href": "escaped_value" } @@ -464,6 +491,9 @@ "rootContentNode": { "_links": { "children": [ + { + "href": "escaped_value" + }, { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set content_nodechecklist_nodes__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set content_nodechecklist_nodes__1.json index f883ff8e4f..9b0796fa6f 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set content_nodechecklist_nodes__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set content_nodechecklist_nodes__1.json @@ -1,6 +1,5 @@ { "_links": { - "checklist": "escaped_value", "checklistItems": [], "children": [], "contentType": { diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 67a199a93c..a58a5d5a5c 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -8315,13 +8315,6 @@ components: deprecated: false description: '' properties: - checklist: - description: 'The Checklist this Item belongs to.' - example: /checklists/1a2b3c4d - format: iri-reference - type: - - 'null' - - string checklistItems: description: 'The content types that are most likely to be useful for planning programme of this category.' example: '["/checklist_items/1a2b3c4d"]' @@ -8429,21 +8422,6 @@ components: type: - array - 'null' - checklist: - description: 'The Checklist this Item belongs to.' - example: /checklists/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - checklistItems: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/checklist_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array contentType: description: |- Defines the type of this content node. There is a fixed list of types that are implemented @@ -8504,7 +8482,6 @@ components: - 'null' - string required: - - checklistItems - contentType - parent - position @@ -8520,21 +8497,6 @@ components: type: - array - 'null' - checklist: - description: 'The Checklist this Item belongs to.' - example: /checklists/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - checklistItems: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/checklist_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array data: description: 'Holds the actual data of the content node.' example: @@ -8587,7 +8549,6 @@ components: - 'null' - string required: - - checklistItems - position type: object ChecklistNode.jsonapi: @@ -8602,13 +8563,6 @@ components: - array - 'null' writeOnly: true - checklist: - description: 'The Checklist this Item belongs to.' - example: /checklists/1a2b3c4d - format: iri-reference - type: - - 'null' - - string checklistItems: description: 'The content types that are most likely to be useful for planning programme of this category.' example: '["/checklist_items/1a2b3c4d"]' @@ -8616,6 +8570,7 @@ components: example: 'https://example.com/' format: iri-reference type: string + readOnly: true type: array children: description: 'All content nodes that are direct children of this content node.' @@ -8727,13 +8682,6 @@ components: type: string type: object type: object - checklist: - description: 'The Checklist this Item belongs to.' - example: /checklists/1a2b3c4d - format: iri-reference - type: - - 'null' - - string checklistItems: description: 'The content types that are most likely to be useful for planning programme of this category.' example: '["/checklist_items/1a2b3c4d"]' @@ -8850,21 +8798,6 @@ components: type: - array - 'null' - checklist: - description: 'The Checklist this Item belongs to.' - example: /checklists/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - checklistItems: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/checklist_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array contentType: description: |- Defines the type of this content node. There is a fixed list of types that are implemented @@ -8925,7 +8858,6 @@ components: - 'null' - string required: - - checklistItems - contentType - parent - position @@ -8957,13 +8889,6 @@ components: '@type': readOnly: true type: string - checklist: - description: 'The Checklist this Item belongs to.' - example: /checklists/1a2b3c4d - format: iri-reference - type: - - 'null' - - string checklistItems: description: 'The content types that are most likely to be useful for planning programme of this category.' example: '["/checklist_items/1a2b3c4d"]' @@ -9071,21 +8996,6 @@ components: type: - array - 'null' - checklist: - description: 'The Checklist this Item belongs to.' - example: /checklists/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - checklistItems: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/checklist_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array contentType: description: |- Defines the type of this content node. There is a fixed list of types that are implemented @@ -9146,7 +9056,6 @@ components: - 'null' - string required: - - checklistItems - contentType - parent - position diff --git a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml index cffd038675..85260de4e7 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml @@ -1,5 +1,5 @@ /activities: 21 -/activities/item: 33 +/activities/item: 36 /activity_progress_labels: 6 /activity_progress_labels/item: 7 /activity_responsibles: 6 From 3b67e15e4c04eb817eff9d469e17d0732c604e49 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Fri, 21 Jun 2024 22:09:27 +0200 Subject: [PATCH 09/30] UnitTest: ChecklistNode add/remove ChecklistItem --- api/fixtures/checklistNodes.yml | 2 + ...16104153.php => Version20240620104153.php} | 2 +- ...16143000.php => Version20240620143000.php} | 2 +- .../schema/Version20240621195713.php | 34 +++++++ api/src/Entity/ContentNode/ChecklistNode.php | 4 +- .../ChecklistNode/UpdateChecklistNodeTest.php | 89 +++++++++++++++++++ ...ta set content_nodechecklist_nodes__1.json | 6 +- ...ucture with data set content_nodes__1.json | 6 +- ...Structure with data set activities__1.json | 6 +- 9 files changed, 144 insertions(+), 7 deletions(-) rename api/migrations/schema/{Version20240616104153.php => Version20240620104153.php} (96%) rename api/migrations/schema/{Version20240616143000.php => Version20240620143000.php} (93%) create mode 100644 api/migrations/schema/Version20240621195713.php diff --git a/api/fixtures/checklistNodes.yml b/api/fixtures/checklistNodes.yml index eb7ffe74cc..a6af5b9eeb 100644 --- a/api/fixtures/checklistNodes.yml +++ b/api/fixtures/checklistNodes.yml @@ -6,6 +6,8 @@ App\Entity\ContentNode\ChecklistNode: position: 1 instanceName: contentType: '@contentTypeChecklist' + checklistItems: + - '@checklistItem1_1_1' checklistNode3: root: '@columnLayout3' parent: '@columnLayout3' diff --git a/api/migrations/schema/Version20240616104153.php b/api/migrations/schema/Version20240620104153.php similarity index 96% rename from api/migrations/schema/Version20240616104153.php rename to api/migrations/schema/Version20240620104153.php index 340d9d16fd..7e0da4b8a2 100644 --- a/api/migrations/schema/Version20240616104153.php +++ b/api/migrations/schema/Version20240620104153.php @@ -10,7 +10,7 @@ /** * Auto-generated Migration: Please modify to your needs! */ -final class Version20240616104153 extends AbstractMigration { +final class Version20240620104153 extends AbstractMigration { public function getDescription(): string { return ''; } diff --git a/api/migrations/schema/Version20240616143000.php b/api/migrations/schema/Version20240620143000.php similarity index 93% rename from api/migrations/schema/Version20240616143000.php rename to api/migrations/schema/Version20240620143000.php index f18be0dcb8..d6c0c34edf 100644 --- a/api/migrations/schema/Version20240616143000.php +++ b/api/migrations/schema/Version20240620143000.php @@ -7,7 +7,7 @@ use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; -final class Version20240616143000 extends AbstractMigration { +final class Version20240620143000 extends AbstractMigration { public function getDescription(): string { return 'Add ChecklistNode content type'; } diff --git a/api/migrations/schema/Version20240621195713.php b/api/migrations/schema/Version20240621195713.php new file mode 100644 index 0000000000..3dce8330ff --- /dev/null +++ b/api/migrations/schema/Version20240621195713.php @@ -0,0 +1,34 @@ +addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT FK_5A2B5B31DE6B6F00'); + $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT FK_5A2B5B318A09A289'); + $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B31DE6B6F00 FOREIGN KEY (checklistnode_id) REFERENCES content_node (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B318A09A289 FOREIGN KEY (checklistitem_id) REFERENCES checklist_item (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT fk_5a2b5b31de6b6f00'); + $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT fk_5a2b5b318a09a289'); + $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT fk_5a2b5b31de6b6f00 FOREIGN KEY (checklistnode_id) REFERENCES content_node (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT fk_5a2b5b318a09a289 FOREIGN KEY (checklistitem_id) REFERENCES checklist_item (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } +} diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php index 237296a7b9..36713bfb93 100644 --- a/api/src/Entity/ContentNode/ChecklistNode.php +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -57,8 +57,8 @@ class ChecklistNode extends ContentNode { #[Groups(['read'])] #[ORM\ManyToMany(targetEntity: ChecklistItem::class, inversedBy: 'checklistNodes')] #[ORM\JoinTable(name: 'checklistnode_checklistitem')] - #[ORM\JoinColumn(name: 'checklistnode_id', referencedColumnName: 'id')] - #[ORM\InverseJoinColumn(name: 'checklistitem_id', referencedColumnName: 'id')] + #[ORM\JoinColumn(name: 'checklistnode_id', referencedColumnName: 'id', onDelete: 'CASCADE')] + #[ORM\InverseJoinColumn(name: 'checklistitem_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[ORM\OrderBy(['position' => 'ASC'])] public Collection $checklistItems; diff --git a/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php b/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php index 182a81d0d5..b6c5661558 100644 --- a/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php +++ b/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php @@ -2,6 +2,7 @@ namespace App\Tests\Api\ContentNodes\ChecklistNode; +use App\Entity\ContentNode\ChecklistNode; use App\Tests\Api\ContentNodes\UpdateContentNodeTestCase; /** @@ -14,4 +15,92 @@ public function setUp(): void { $this->endpoint = '/content_node/checklist_nodes'; $this->defaultEntity = static::getFixture('checklistNode1'); } + + public function testAddChecklistItemIsDeniedForGuest() { + $checklistItemId = static::getFixture('checklistItem1_1_2')->getId(); + static::createClientWithCredentials(['email' => static::getFixture('user3guest')->getEmail()]) + ->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ + 'addChecklistItemIds' => [$checklistItemId], + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testAddChecklistItemIsDeniedForMember() { + $checklistItemId = static::getFixture('checklistItem1_1_2')->getId(); + static::createClientWithCredentials(['email' => static::getFixture('user2member')->getEmail()]) + ->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ + 'addChecklistItemIds' => [$checklistItemId], + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + '_links' => [ + 'checklistItems' => [ + 1 => [ + 'href' => '/checklist_items/'.$checklistItemId, + ], + ], + ], + ]); + } + + public function testAddChecklistItemIsDeniedForManager() { + $checklistItemId = static::getFixture('checklistItem1_1_2')->getId(); + static::createClientWithCredentials()->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ + 'addChecklistItemIds' => [$checklistItemId], + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + '_links' => [ + 'checklistItems' => [ + 1 => [ + 'href' => '/checklist_items/'.$checklistItemId, + ], + ], + ], + ]); + } + + public function testRemoveChecklistItemIsDeniedForGuest() { + $checklistItemId = static::getFixture('checklistItem1_1_1')->getId(); + static::createClientWithCredentials(['email' => static::getFixture('user3guest')->getEmail()]) + ->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ + 'removeChecklistItemIds' => [$checklistItemId], + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(403); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'Access Denied.', + ]); + } + + public function testRemoveChecklistItemIsDeniedForMember() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials(['email' => static::getFixture('user2member')->getEmail()]) + ->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ + 'removeChecklistItemIds' => [$checklistItem->getId()], + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(200); + $checklistNode = $this->getEntityManager()->getRepository(ChecklistNode::class)->find($this->defaultEntity->getId()); + $this->assertFalse(in_array($checklistItem, $checklistNode->getChecklistItems())); + } + + public function testRemoveChecklistItemIsDeniedForManager() { + $checklistItem = static::getFixture('checklistItem1_1_1'); + static::createClientWithCredentials()->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ + 'removeChecklistItemIds' => [$checklistItem->getId()], + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]); + + $this->assertResponseStatusCodeSame(200); + $checklistNode = $this->getEntityManager()->getRepository(ChecklistNode::class)->find($this->defaultEntity->getId()); + $this->assertFalse(in_array($checklistItem, $checklistNode->getChecklistItems())); + } } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json index d92c7c8ab7..20693aaa9d 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodechecklist_nodes__1.json @@ -27,7 +27,11 @@ }, { "_links": { - "checklistItems": [], + "checklistItems": [ + { + "href": "escaped_value" + } + ], "children": [], "contentType": { "href": "escaped_value" diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json index 87fb09384c..2088e60717 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json @@ -104,7 +104,11 @@ }, { "_links": { - "checklistItems": [], + "checklistItems": [ + { + "href": "escaped_value" + } + ], "children": [], "contentType": { "href": "escaped_value" diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set activities__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set activities__1.json index 81d49812a7..df948bb2cf 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set activities__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set activities__1.json @@ -125,7 +125,11 @@ }, { "_links": { - "checklistItems": [], + "checklistItems": [ + { + "href": "escaped_value" + } + ], "children": [], "contentType": { "href": "escaped_value" From 8151ebca8855da43d07023af6e1e4f56e8e89ba9 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Sat, 22 Jun 2024 14:42:51 +0200 Subject: [PATCH 10/30] unify AssertNoLoop-Validators --- api/src/Entity/ChecklistItem.php | 8 +++- api/src/Entity/ContentNode.php | 8 +++- api/src/Entity/HasParentInterface.php | 7 +++ .../{ContentNode => }/AssertNoLoop.php | 2 +- .../AssertNoLoopValidator.php | 8 ++-- .../Validator/ChecklistItem/AssertNoLoop.php | 10 ---- .../ChecklistItem/AssertNoLoopValidator.php | 46 ------------------- .../AssertNoLoopValidatorTest.php | 6 +-- 8 files changed, 27 insertions(+), 68 deletions(-) create mode 100644 api/src/Entity/HasParentInterface.php rename api/src/Validator/{ContentNode => }/AssertNoLoop.php (83%) rename api/src/Validator/{ContentNode => }/AssertNoLoopValidator.php (88%) delete mode 100644 api/src/Validator/ChecklistItem/AssertNoLoop.php delete mode 100644 api/src/Validator/ChecklistItem/AssertNoLoopValidator.php rename api/tests/Validator/{ContentNode => }/AssertNoLoopValidatorTest.php (95%) diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php index c6c453806b..f9a42a65c0 100644 --- a/api/src/Entity/ChecklistItem.php +++ b/api/src/Entity/ChecklistItem.php @@ -16,8 +16,8 @@ use App\InputFilter; use App\Repository\ChecklistItemRepository; use App\Util\EntityMap; +use App\Validator\AssertNoLoop; use App\Validator\ChecklistItem\AssertBelongsToChecklist; -use App\Validator\ChecklistItem\AssertNoLoop; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -67,7 +67,7 @@ )] #[ApiFilter(filterClass: SearchFilter::class, properties: ['checklist'])] #[ORM\Entity(repositoryClass: ChecklistItemRepository::class)] -class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface { +class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface, HasParentInterface { public const CHECKLIST_SUBRESOURCE_URI_TEMPLATE = '/checklists/{checklistId}/checklist_items.{_format}'; /** @@ -140,6 +140,10 @@ public function getCamp(): ?Camp { return $this->checklist?->getCamp(); } + public function getParent(): ?HasParentInterface { + return $this->parent; + } + /** * @return ChecklistItem[] */ diff --git a/api/src/Entity/ContentNode.php b/api/src/Entity/ContentNode.php index 0b29d17753..7c40ff24d4 100644 --- a/api/src/Entity/ContentNode.php +++ b/api/src/Entity/ContentNode.php @@ -14,9 +14,9 @@ use App\Util\ClassInfoTrait; use App\Util\EntityMap; use App\Util\JsonMergePatch; +use App\Validator\AssertNoLoop; use App\Validator\ContentNode\AssertAttachedToRoot; use App\Validator\ContentNode\AssertContentTypeCompatible; -use App\Validator\ContentNode\AssertNoLoop; use App\Validator\ContentNode\AssertNoRootChange; use App\Validator\ContentNode\AssertSlotSupportedByParent; use Doctrine\Common\Collections\ArrayCollection; @@ -49,7 +49,7 @@ #[ORM\InheritanceType('SINGLE_TABLE')] #[ORM\DiscriminatorColumn(name: 'strategy', type: 'string')] #[ORM\UniqueConstraint(name: 'contentnode_parentid_slot_position_unique', columns: ['parentid', 'slot', 'position'])] -abstract class ContentNode extends BaseEntity implements BelongsToContentNodeTreeInterface, CopyFromPrototypeInterface { +abstract class ContentNode extends BaseEntity implements BelongsToContentNodeTreeInterface, CopyFromPrototypeInterface, HasParentInterface { use ClassInfoTrait; /** @@ -180,6 +180,10 @@ public function getRoot(): ?ColumnLayout { return $this->root; } + public function getParent(): ?HasParentInterface { + return $this->parent; + } + /** * Holds the actual data of the content node. */ diff --git a/api/src/Entity/HasParentInterface.php b/api/src/Entity/HasParentInterface.php new file mode 100644 index 0000000000..87ec5c2d00 --- /dev/null +++ b/api/src/Entity/HasParentInterface.php @@ -0,0 +1,7 @@ +context->getObject(); - /** @var ContentNode $parent */ + /** @var HasParentInterface $parent */ $parent = $value; // $seen keeps track of all parents that we have visited. This is for a safety @@ -36,7 +36,7 @@ public function validate($value, Constraint $constraint): void { } $seen[] = $parent->getId(); - $parent = $parent->parent; + $parent = $parent->getParent(); } } } diff --git a/api/src/Validator/ChecklistItem/AssertNoLoop.php b/api/src/Validator/ChecklistItem/AssertNoLoop.php deleted file mode 100644 index f60e2bba67..0000000000 --- a/api/src/Validator/ChecklistItem/AssertNoLoop.php +++ /dev/null @@ -1,10 +0,0 @@ -context->getObject(); - if (!$object instanceof ChecklistItem) { - throw new UnexpectedValueException($object, ChecklistItem::class); - } - - /** @var ChecklistItem $parent */ - $parent = $value; - - // $seen keeps track of all parents that we have visited. This is for a safety - // bailout mechanism to avoid an infinite loop in case there is flawed data in the DB - $seen = []; - - while (null !== $parent && !in_array($parent->getId(), $seen)) { - if ($parent->getId() === $object->getId()) { - $this->context->buildViolation($constraint->message) - ->addViolation() - ; - - return; - } - - $seen[] = $parent->getId(); - $parent = $parent->parent; - } - } -} diff --git a/api/tests/Validator/ContentNode/AssertNoLoopValidatorTest.php b/api/tests/Validator/AssertNoLoopValidatorTest.php similarity index 95% rename from api/tests/Validator/ContentNode/AssertNoLoopValidatorTest.php rename to api/tests/Validator/AssertNoLoopValidatorTest.php index ead7803414..527f4bfb94 100644 --- a/api/tests/Validator/ContentNode/AssertNoLoopValidatorTest.php +++ b/api/tests/Validator/AssertNoLoopValidatorTest.php @@ -1,10 +1,10 @@ Date: Tue, 9 Jul 2024 19:26:00 +0200 Subject: [PATCH 11/30] fixes according to review --- api/fixtures/checklistItems.yml | 3 --- api/migrations/schema/Version20240620143000.php | 2 +- api/src/Entity/Checklist.php | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/api/fixtures/checklistItems.yml b/api/fixtures/checklistItems.yml index 3892d00a43..ffe2553f24 100644 --- a/api/fixtures/checklistItems.yml +++ b/api/fixtures/checklistItems.yml @@ -9,9 +9,6 @@ App\Entity\ChecklistItem: checklist: '@checklist1' parent: '@checklistItem1_1_2' text: 'Camp1_List1_Item2_Item3' -# checklist2WithNoItems: -# checklist: '@checklist2WithNoItems' -# text: 'Camp1_List2_Item1' checklistItem2_1_1: checklist: '@checklist1camp2' text: 'Camp2_List1_Item1' diff --git a/api/migrations/schema/Version20240620143000.php b/api/migrations/schema/Version20240620143000.php index d6c0c34edf..1d61764ed0 100644 --- a/api/migrations/schema/Version20240620143000.php +++ b/api/migrations/schema/Version20240620143000.php @@ -29,6 +29,6 @@ public function up(Schema $schema): void { public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DELETE FROM public.content_type WHERE id IN (\'a4211c11211c\')'); + $this->addSql("DELETE FROM public.content_type WHERE id IN ('a4211c11211c')"); } } diff --git a/api/src/Entity/Checklist.php b/api/src/Entity/Checklist.php index 9a5ba7fd2c..9703f5d30a 100644 --- a/api/src/Entity/Checklist.php +++ b/api/src/Entity/Checklist.php @@ -102,7 +102,7 @@ class Checklist extends BaseEntity implements BelongsToCampInterface, CopyFromPr #[Assert\NotBlank] #[Assert\Length(max: 32)] #[ORM\Column(type: 'text')] - public ?string $name = null; + public string $name; public function __construct() { parent::__construct(); From 0cee81e5833f9e776e8b1c731bce7ccfacfb2405 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Tue, 9 Jul 2024 19:32:18 +0200 Subject: [PATCH 12/30] fixes according to review --- .../schema/Version20240620104153.php | 6 ++-- .../schema/Version20240621195713.php | 34 ------------------- api/src/Entity/ChecklistItem.php | 2 ++ .../ChecklistNodePersistProcessor.php | 10 ++++-- .../CreateChecklistItemTest.php | 8 ++--- .../ChecklistNode/CreateChecklistNodeTest.php | 10 ++---- 6 files changed, 20 insertions(+), 50 deletions(-) delete mode 100644 api/migrations/schema/Version20240621195713.php diff --git a/api/migrations/schema/Version20240620104153.php b/api/migrations/schema/Version20240620104153.php index 7e0da4b8a2..82cfd58b19 100644 --- a/api/migrations/schema/Version20240620104153.php +++ b/api/migrations/schema/Version20240620104153.php @@ -17,16 +17,18 @@ public function getDescription(): string { public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE UNIQUE INDEX checklistitem_checklistid_parentid_position_unique ON checklist_item (checklistid, parentid, position)'); $this->addSql('CREATE TABLE checklistnode_checklistitem (checklistnode_id VARCHAR(16) NOT NULL, checklistitem_id VARCHAR(16) NOT NULL, PRIMARY KEY(checklistnode_id, checklistitem_id))'); $this->addSql('CREATE INDEX IDX_5A2B5B31DE6B6F00 ON checklistnode_checklistitem (checklistnode_id)'); $this->addSql('CREATE INDEX IDX_5A2B5B318A09A289 ON checklistnode_checklistitem (checklistitem_id)'); - $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B31DE6B6F00 FOREIGN KEY (checklistnode_id) REFERENCES content_node (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B318A09A289 FOREIGN KEY (checklistitem_id) REFERENCES checklist_item (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B31DE6B6F00 FOREIGN KEY (checklistnode_id) REFERENCES content_node (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B318A09A289 FOREIGN KEY (checklistitem_id) REFERENCES checklist_item (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); } public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP INDEX checklistitem_checklistid_parentid_position_unique'); $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT FK_5A2B5B31DE6B6F00'); $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT FK_5A2B5B318A09A289'); $this->addSql('DROP TABLE checklistnode_checklistitem'); diff --git a/api/migrations/schema/Version20240621195713.php b/api/migrations/schema/Version20240621195713.php deleted file mode 100644 index 3dce8330ff..0000000000 --- a/api/migrations/schema/Version20240621195713.php +++ /dev/null @@ -1,34 +0,0 @@ -addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT FK_5A2B5B31DE6B6F00'); - $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT FK_5A2B5B318A09A289'); - $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B31DE6B6F00 FOREIGN KEY (checklistnode_id) REFERENCES content_node (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT FK_5A2B5B318A09A289 FOREIGN KEY (checklistitem_id) REFERENCES checklist_item (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); - } - - public function down(Schema $schema): void { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE SCHEMA public'); - $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT fk_5a2b5b31de6b6f00'); - $this->addSql('ALTER TABLE checklistnode_checklistitem DROP CONSTRAINT fk_5a2b5b318a09a289'); - $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT fk_5a2b5b31de6b6f00 FOREIGN KEY (checklistnode_id) REFERENCES content_node (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - $this->addSql('ALTER TABLE checklistnode_checklistitem ADD CONSTRAINT fk_5a2b5b318a09a289 FOREIGN KEY (checklistitem_id) REFERENCES checklist_item (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); - } -} diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php index f9a42a65c0..2b726bf253 100644 --- a/api/src/Entity/ChecklistItem.php +++ b/api/src/Entity/ChecklistItem.php @@ -67,6 +67,7 @@ )] #[ApiFilter(filterClass: SearchFilter::class, properties: ['checklist'])] #[ORM\Entity(repositoryClass: ChecklistItemRepository::class)] +#[ORM\UniqueConstraint(name: 'checklistitem_checklistid_parentid_position_unique', columns: ['checklistid', 'parentid', 'position'])] class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface, HasParentInterface { public const CHECKLIST_SUBRESOURCE_URI_TEMPLATE = '/checklists/{checklistId}/checklist_items.{_format}'; @@ -74,6 +75,7 @@ class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFr * The Checklist this Item belongs to. */ #[ApiProperty(example: '/checklists/1a2b3c4d')] + #[Gedmo\SortableGroup] #[Groups(['read', 'create'])] #[ORM\ManyToOne(targetEntity: Checklist::class, inversedBy: 'checklistItems')] #[ORM\JoinColumn(nullable: false, onDelete: 'cascade')] diff --git a/api/src/State/ContentNode/ChecklistNodePersistProcessor.php b/api/src/State/ContentNode/ChecklistNodePersistProcessor.php index b138cf7b73..bdbe6a9f34 100644 --- a/api/src/State/ContentNode/ChecklistNodePersistProcessor.php +++ b/api/src/State/ContentNode/ChecklistNodePersistProcessor.php @@ -25,13 +25,19 @@ public function onBefore($data, Operation $operation, array $uriVariables = [], if (null !== $data->addChecklistItemIds) { foreach ($data->addChecklistItemIds as $checklistItemId) { $checklistItem = $this->checklistItemRepository->find($checklistItemId); - $data->addChecklistItem($checklistItem); + if (null != $checklistItem) { + // if a checklistItem does not exists, do not add it + $data->addChecklistItem($checklistItem); + } } } if (null !== $data->removeChecklistItemIds) { foreach ($data->removeChecklistItemIds as $checklistItemId) { $checklistItem = $this->checklistItemRepository->find($checklistItemId); - $data->removeChecklistItem($checklistItem); + if (null != $checklistItem) { + // if a checklistItem no longer exists, it does not have to be removed + $data->removeChecklistItem($checklistItem); + } } } diff --git a/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php b/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php index 007d8d0970..6aa37d9489 100644 --- a/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php +++ b/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php @@ -66,14 +66,14 @@ public function testCreateChecklistItemIsAllowedForMember() { ; $this->assertResponseStatusCodeSame(201); - $this->assertJsonContains($this->getExampleReadPayload(['position' => 5])); + $this->assertJsonContains($this->getExampleReadPayload(['position' => 2])); } public function testCreateChecklistItemIsAllowedForManager() { static::createClientWithCredentials()->request('POST', '/checklist_items', ['json' => $this->getExampleWritePayload()]); $this->assertResponseStatusCodeSame(201); - $this->assertJsonContains($this->getExampleReadPayload(['position' => 5])); + $this->assertJsonContains($this->getExampleReadPayload(['position' => 2])); } public function testCreateChecklistItemInCampPrototypeIsDeniedForUnrelatedUser() { @@ -181,7 +181,7 @@ public function testCreateChecklistItemTrimsText() { $this->assertJsonContains($this->getExampleReadPayload( [ 'text' => 'Ziel 1', - 'position' => 5, + 'position' => 2, ] )); } @@ -203,7 +203,7 @@ public function testCreateChecklistItemCleansForbiddenCharactersFromText() { $this->assertJsonContains($this->getExampleReadPayload( [ 'text' => 'Ziel 1', - 'position' => 5, + 'position' => 2, ] )); } diff --git a/api/tests/Api/ContentNodes/ChecklistNode/CreateChecklistNodeTest.php b/api/tests/Api/ContentNodes/ChecklistNode/CreateChecklistNodeTest.php index 703e19bbe8..8d0fd4d8f8 100644 --- a/api/tests/Api/ContentNodes/ChecklistNode/CreateChecklistNodeTest.php +++ b/api/tests/Api/ContentNodes/ChecklistNode/CreateChecklistNodeTest.php @@ -22,14 +22,8 @@ public function setUp(): void { */ public function getExampleWritePayload($attributes = [], $except = []) { return parent::getExampleWritePayload( - array_merge( - [ - 'addChecklistItemIds' => null, - 'removeChecklistItemIds' => null, - ], - $attributes - ), - $except + $attributes, + array_merge(['addChecklistItemIds', 'removeChecklistItemIds'], $except) ); } } From 2297d5f9616cf903554c00233d33a09fb4e21202 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Sat, 13 Jul 2024 16:36:45 +0200 Subject: [PATCH 13/30] ChecklistItem text length = 256 --- api/src/Entity/ChecklistItem.php | 2 +- .../ChecklistItems/CreateChecklistItemTest.php | 4 ++-- .../ChecklistItems/UpdateChecklistItemTest.php | 4 ++-- ...otTest__testOpenApiSpecMatchesSnapshot__1.yml | 16 ++++++++-------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php index 2b726bf253..9cbec270c1 100644 --- a/api/src/Entity/ChecklistItem.php +++ b/api/src/Entity/ChecklistItem.php @@ -117,7 +117,7 @@ class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFr #[InputFilter\Trim] #[InputFilter\CleanText] #[Assert\NotBlank] - #[Assert\Length(max: 64)] + #[Assert\Length(max: 256)] #[ORM\Column(type: 'text')] public ?string $text = null; diff --git a/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php b/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php index 6aa37d9489..6381cc0999 100644 --- a/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php +++ b/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php @@ -147,7 +147,7 @@ public function testCreateChecklistItemValidatesTooLongText() { [ 'json' => $this->getExampleWritePayload( [ - 'text' => str_repeat('l', 65), + 'text' => str_repeat('l', 257), ] ), ] @@ -158,7 +158,7 @@ public function testCreateChecklistItemValidatesTooLongText() { 'violations' => [ [ 'propertyPath' => 'text', - 'message' => 'This value is too long. It should have 64 characters or less.', + 'message' => 'This value is too long. It should have 256 characters or less.', ], ], ]); diff --git a/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php b/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php index ca6ceae23a..0152401f68 100644 --- a/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php +++ b/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php @@ -161,7 +161,7 @@ public function testPatchChecklistItemValidatesTooLongText() { '/checklist_items/'.$checklistItem->getId(), [ 'json' => [ - 'text' => str_repeat('l', 65), + 'text' => str_repeat('l', 257), ], 'headers' => ['Content-Type' => 'application/merge-patch+json'], ] @@ -172,7 +172,7 @@ public function testPatchChecklistItemValidatesTooLongText() { 'violations' => [ [ 'propertyPath' => 'text', - 'message' => 'This value is too long. It should have 64 characters or less.', + 'message' => 'This value is too long. It should have 256 characters or less.', ], ], ]); diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index a58a5d5a5c..26a4f2af89 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -7955,7 +7955,7 @@ components: text: description: 'The human readable text of the checklist-item.' example: Pfaditechnick - maxLength: 64 + maxLength: 256 type: string required: - checklist @@ -7987,7 +7987,7 @@ components: text: description: 'The human readable text of the checklist-item.' example: Pfaditechnick - maxLength: 64 + maxLength: 256 type: string required: - position @@ -8022,7 +8022,7 @@ components: text: description: 'The human readable text of the checklist-item.' example: Pfaditechnick - maxLength: 64 + maxLength: 256 type: string required: - checklist @@ -8053,7 +8053,7 @@ components: text: description: 'The human readable text of the checklist-item.' example: Pfaditechnick - maxLength: 64 + maxLength: 256 type: string required: - position @@ -8147,7 +8147,7 @@ components: text: description: 'The human readable text of the checklist-item.' example: Pfaditechnick - maxLength: 64 + maxLength: 256 type: string required: - checklist @@ -8193,7 +8193,7 @@ components: text: description: 'The human readable text of the checklist-item.' example: Pfaditechnick - maxLength: 64 + maxLength: 256 type: string required: - checklist @@ -8267,7 +8267,7 @@ components: text: description: 'The human readable text of the checklist-item.' example: Pfaditechnick - maxLength: 64 + maxLength: 256 type: string required: - checklist @@ -8304,7 +8304,7 @@ components: text: description: 'The human readable text of the checklist-item.' example: Pfaditechnick - maxLength: 64 + maxLength: 256 type: string required: - checklist From e8d58d04d7e15467a3f45587c35b810859a06e68 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Mon, 15 Jul 2024 18:25:41 +0200 Subject: [PATCH 14/30] fix unittest-names --- .../ChecklistNode/UpdateChecklistNodeTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php b/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php index b6c5661558..a2cd67de89 100644 --- a/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php +++ b/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php @@ -30,7 +30,7 @@ public function testAddChecklistItemIsDeniedForGuest() { ]); } - public function testAddChecklistItemIsDeniedForMember() { + public function testAddChecklistItemForMember() { $checklistItemId = static::getFixture('checklistItem1_1_2')->getId(); static::createClientWithCredentials(['email' => static::getFixture('user2member')->getEmail()]) ->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ @@ -49,7 +49,7 @@ public function testAddChecklistItemIsDeniedForMember() { ]); } - public function testAddChecklistItemIsDeniedForManager() { + public function testAddChecklistItemForManager() { $checklistItemId = static::getFixture('checklistItem1_1_2')->getId(); static::createClientWithCredentials()->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ 'addChecklistItemIds' => [$checklistItemId], @@ -81,7 +81,7 @@ public function testRemoveChecklistItemIsDeniedForGuest() { ]); } - public function testRemoveChecklistItemIsDeniedForMember() { + public function testRemoveChecklistItemForMember() { $checklistItem = static::getFixture('checklistItem1_1_1'); static::createClientWithCredentials(['email' => static::getFixture('user2member')->getEmail()]) ->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ @@ -93,7 +93,7 @@ public function testRemoveChecklistItemIsDeniedForMember() { $this->assertFalse(in_array($checklistItem, $checklistNode->getChecklistItems())); } - public function testRemoveChecklistItemIsDeniedForManager() { + public function testRemoveChecklistItemForManager() { $checklistItem = static::getFixture('checklistItem1_1_1'); static::createClientWithCredentials()->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ 'removeChecklistItemIds' => [$checklistItem->getId()], From 09c4000abe9acddb8ce583c170a962fdb7da114b Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Sun, 21 Jul 2024 18:00:34 +0200 Subject: [PATCH 15/30] fix performance-unittests --- .../performance_test/checklistItems.yml | 2 +- ...est__testOpenApiSpecMatchesSnapshot__1.yml | 5 ----- ...manceDidNotChangeForStableEndpoints__1.yml | 20 +++++++++++-------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/api/fixtures/performance_test/checklistItems.yml b/api/fixtures/performance_test/checklistItems.yml index ea01fe011d..623cc12099 100644 --- a/api/fixtures/performance_test/checklistItems.yml +++ b/api/fixtures/performance_test/checklistItems.yml @@ -6,5 +6,5 @@ App\Entity\ChecklistItem: checklist: '@additional_checklist2_' text: 'Item_' additional_checklistItem_camp1_{1..12}: - camp: '@additional_checklist_camp1_1' + checklist: '@additional_checklist_camp1_1' text: 'Item_' diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 26a4f2af89..3a3bc5d0bf 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -24849,7 +24849,6 @@ paths: summary: 'Retrieves the collection of ChecklistItem resources.' tags: - ChecklistItem - parameters: [] post: deprecated: false description: 'Creates a ChecklistItem resource.' @@ -24968,7 +24967,6 @@ paths: summary: 'Retrieves a ChecklistItem resource.' tags: - ChecklistItem - parameters: [] patch: deprecated: false description: 'Updates the ChecklistItem resource.' @@ -25238,7 +25236,6 @@ paths: summary: 'Retrieves the collection of ChecklistItem resources.' tags: - ChecklistItem - parameters: [] '/checklists/{id}': delete: deprecated: false @@ -25475,7 +25472,6 @@ paths: summary: 'Retrieves the collection of ChecklistNode resources.' tags: - ChecklistNode - parameters: [] post: deprecated: false description: 'Creates a ChecklistNode resource.' @@ -25594,7 +25590,6 @@ paths: summary: 'Retrieves a ChecklistNode resource.' tags: - ChecklistNode - parameters: [] patch: deprecated: false description: 'Updates the ChecklistNode resource.' diff --git a/api/tests/Api/SnapshotTests/__snapshots__/performance_test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/performance_test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml index b8edfa35ed..fdc9790ac3 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/performance_test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/performance_test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml @@ -1,15 +1,19 @@ /activities: 221 -/activities/item: 233 +/activities/item: 236 /activity_progress_labels: 6 /activity_progress_labels/item: 7 /activity_responsibles: 6 /activity_responsibles/item: 8 -/camps: 36 -/camps/item: 31 -/camp_collaborations: 22 -/camp_collaborations/item: 24 +/camps: 39 +/camps/item: 32 +/camp_collaborations: 25 +/camp_collaborations/item: 25 /categories: 11 /categories/item: 9 +/checklists: 6 +/checklists/item: 7 +/checklist_items: 6 +/checklist_items/item: 8 /content_types: 6 /content_types/item: 6 /days: 26 @@ -21,7 +25,7 @@ /material_lists: 6 /material_lists/item: 7 /periods: 6 -/periods/item: 27 +/periods/item: 28 /profiles: 6 /profiles/item: 6 /schedule_entries: 23 @@ -30,8 +34,8 @@ '/activities?camp=': 213 '/activity_progress_labels?camp=': 6 '/activity_responsibles?activity.camp=': 6 -'/camp_collaborations?camp=': 12 -'/camp_collaborations?activityResponsibles.activity=': 24 +'/camp_collaborations?camp=': 13 +'/camp_collaborations?activityResponsibles.activity=': 25 '/categories?camp=': 9 '/content_types?categories=': 6 '/day_responsibles?day.period=': 6 From ac9fa3228e1e324cbdb31b5074d1ac1e3b68814c Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Tue, 6 Aug 2024 22:57:28 +0200 Subject: [PATCH 16/30] changes according to review --- api/src/Entity/Camp.php | 2 +- api/src/Entity/Checklist.php | 5 +- api/src/Entity/ChecklistItem.php | 11 +- api/src/Entity/ContentNode/ChecklistNode.php | 2 +- ...t.php => AssertBelongsToSameChecklist.php} | 2 +- ...AssertBelongsToSameChecklistValidator.php} | 6 +- .../ListCampCollaborationsTest.php | 2 +- .../ReadCampCollaborationTest.php | 2 +- ...est__testOpenApiSpecMatchesSnapshot__1.yml | 147 ++++++------------ ...manceDidNotChangeForStableEndpoints__1.yml | 14 +- ...manceDidNotChangeForStableEndpoints__1.yml | 14 +- 11 files changed, 79 insertions(+), 128 deletions(-) rename api/src/Validator/ChecklistItem/{AssertBelongsToChecklist.php => AssertBelongsToSameChecklist.php} (75%) rename api/src/Validator/ChecklistItem/{AssertBelongsToChecklistValidator.php => AssertBelongsToSameChecklistValidator.php} (87%) diff --git a/api/src/Entity/Camp.php b/api/src/Entity/Camp.php index 88a895869c..35295e7956 100644 --- a/api/src/Entity/Camp.php +++ b/api/src/Entity/Camp.php @@ -140,7 +140,7 @@ class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototy * List of all Checklists of this Camp. * Each Checklist is a List of ChecklistItems. */ - #[ApiProperty(writable: false, example: '["/checklists/1a2b3c4d"]')] + #[ApiProperty(writable: false, uriTemplate: Checklist::CAMP_SUBRESOURCE_URI_TEMPLATE)] #[Groups(['read'])] #[ORM\OneToMany(targetEntity: Checklist::class, mappedBy: 'camp', orphanRemoval: true, cascade: ['persist'])] public Collection $checklists; diff --git a/api/src/Entity/Checklist.php b/api/src/Entity/Checklist.php index 9703f5d30a..336177e806 100644 --- a/api/src/Entity/Checklist.php +++ b/api/src/Entity/Checklist.php @@ -48,7 +48,6 @@ securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' ), new GetCollection( - name: 'BelongsToCamp_App\Entity\Checklist_get_collection', uriTemplate: self::CAMP_SUBRESOURCE_URI_TEMPLATE, uriVariables: [ 'campId' => new Link( @@ -66,7 +65,7 @@ #[ApiFilter(filterClass: SearchFilter::class, properties: ['camp'])] #[ORM\Entity(repositoryClass: ChecklistRepository::class)] class Checklist extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface { - public const CAMP_SUBRESOURCE_URI_TEMPLATE = '/camps/{campId}/checklists.{_format}'; + public const CAMP_SUBRESOURCE_URI_TEMPLATE = '/camps/{campId}/checklists{._format}'; /** * The camp this checklist belongs to. @@ -87,7 +86,7 @@ class Checklist extends BaseEntity implements BelongsToCampInterface, CopyFromPr /** * All ChecklistItems that belong to this Checklist. */ - #[ApiProperty(writable: false, example: '["/checklist_items/1a2b3c4d"]')] + #[ApiProperty(writable: false, uriTemplate: ChecklistItem::CHECKLIST_SUBRESOURCE_URI_TEMPLATE)] #[Groups(['read'])] #[ORM\OneToMany(targetEntity: ChecklistItem::class, mappedBy: 'checklist', cascade: ['persist'])] public Collection $checklistItems; diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php index 9cbec270c1..9317985251 100644 --- a/api/src/Entity/ChecklistItem.php +++ b/api/src/Entity/ChecklistItem.php @@ -17,7 +17,7 @@ use App\Repository\ChecklistItemRepository; use App\Util\EntityMap; use App\Validator\AssertNoLoop; -use App\Validator\ChecklistItem\AssertBelongsToChecklist; +use App\Validator\ChecklistItem\AssertBelongsToSameChecklist; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -38,9 +38,7 @@ security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' ), new Delete( - security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)', - validate: true, - validationContext: ['groups' => ['delete']] + security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' ), new GetCollection( security: 'is_authenticated()' @@ -50,7 +48,6 @@ securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' ), new GetCollection( - name: 'BelongsToChecklist_App\Entity\ChecklistItem_get_collection', uriTemplate: self::CHECKLIST_SUBRESOURCE_URI_TEMPLATE, uriVariables: [ 'checklistId' => new Link( @@ -69,7 +66,7 @@ #[ORM\Entity(repositoryClass: ChecklistItemRepository::class)] #[ORM\UniqueConstraint(name: 'checklistitem_checklistid_parentid_position_unique', columns: ['checklistid', 'parentid', 'position'])] class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface, HasParentInterface { - public const CHECKLIST_SUBRESOURCE_URI_TEMPLATE = '/checklists/{checklistId}/checklist_items.{_format}'; + public const CHECKLIST_SUBRESOURCE_URI_TEMPLATE = '/checklists/{checklistId}/checklist_items{._format}'; /** * The Checklist this Item belongs to. @@ -86,7 +83,7 @@ class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFr * root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long * as the new parent is in the same checklist as the old one. */ - #[AssertBelongsToChecklist(groups: ['update'])] + #[AssertBelongsToSameChecklist(groups: ['update'])] #[AssertNoLoop(groups: ['update'])] #[ApiProperty(example: '/checklist_items/1a2b3c4d')] #[Gedmo\SortableGroup] diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php index 36713bfb93..433be7ec36 100644 --- a/api/src/Entity/ContentNode/ChecklistNode.php +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -51,7 +51,7 @@ #[ORM\Entity(repositoryClass: ChecklistNodeRepository::class)] class ChecklistNode extends ContentNode { /** - * The content types that are most likely to be useful for planning programme of this category. + * List of selected ChecklistItems. */ #[ApiProperty(example: '["/checklist_items/1a2b3c4d"]')] #[Groups(['read'])] diff --git a/api/src/Validator/ChecklistItem/AssertBelongsToChecklist.php b/api/src/Validator/ChecklistItem/AssertBelongsToSameChecklist.php similarity index 75% rename from api/src/Validator/ChecklistItem/AssertBelongsToChecklist.php rename to api/src/Validator/ChecklistItem/AssertBelongsToSameChecklist.php index ceb67ccc17..a70685628d 100644 --- a/api/src/Validator/ChecklistItem/AssertBelongsToChecklist.php +++ b/api/src/Validator/ChecklistItem/AssertBelongsToSameChecklist.php @@ -5,6 +5,6 @@ use Symfony\Component\Validator\Constraint; #[\Attribute] -class AssertBelongsToChecklist extends Constraint { +class AssertBelongsToSameChecklist extends Constraint { public string $message = 'Must belong to the same checklist.'; } diff --git a/api/src/Validator/ChecklistItem/AssertBelongsToChecklistValidator.php b/api/src/Validator/ChecklistItem/AssertBelongsToSameChecklistValidator.php similarity index 87% rename from api/src/Validator/ChecklistItem/AssertBelongsToChecklistValidator.php rename to api/src/Validator/ChecklistItem/AssertBelongsToSameChecklistValidator.php index 01aaa2fb76..7ad21d1670 100644 --- a/api/src/Validator/ChecklistItem/AssertBelongsToChecklistValidator.php +++ b/api/src/Validator/ChecklistItem/AssertBelongsToSameChecklistValidator.php @@ -9,12 +9,12 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedValueException; -class AssertBelongsToChecklistValidator extends ConstraintValidator { +class AssertBelongsToSameChecklistValidator extends ConstraintValidator { public function __construct(public RequestStack $requestStack) {} public function validate($value, Constraint $constraint): void { - if (!$constraint instanceof AssertBelongsToChecklist) { - throw new UnexpectedTypeException($constraint, AssertBelongsToChecklist::class); + if (!$constraint instanceof AssertBelongsToSameChecklist) { + throw new UnexpectedTypeException($constraint, AssertBelongsToSameChecklist::class); } if (null === $value || '' === $value) { diff --git a/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php b/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php index 8ce744e353..06975241fe 100644 --- a/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php +++ b/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php @@ -114,6 +114,6 @@ public function testSqlQueryCount() { $client->enableProfiler(); $client->request('GET', '/camp_collaborations'); - $this->assertSqlQueryCount($client, 25); + $this->assertSqlQueryCount($client, 22); } } diff --git a/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php b/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php index 8c9eaf027f..a9916e6211 100644 --- a/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php +++ b/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php @@ -126,6 +126,6 @@ public function testSqlQueryCount() { $client->enableProfiler(); $client->request('GET', '/camp_collaborations/'.$campCollaboration->getId()); - $this->assertSqlQueryCount($client, 15); + $this->assertSqlQueryCount($client, 14); } } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 3a3bc5d0bf..c3eec18fe1 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -2335,13 +2335,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -2538,13 +2535,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -2730,13 +2724,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -2929,13 +2920,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3540,13 +3528,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3752,13 +3737,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3953,13 +3935,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4161,13 +4140,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4516,13 +4492,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4742,13 +4715,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4957,13 +4927,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -5179,13 +5146,10 @@ components: type: string checklists: description: 'List of all Checklists of this Camp.' - example: '["/checklists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -7625,13 +7589,10 @@ components: type: string checklistItems: description: 'All ChecklistItems that belong to this Checklist.' - example: '["/checklist_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d @@ -7766,13 +7727,10 @@ components: type: string checklistItems: description: 'All ChecklistItems that belong to this Checklist.' - example: '["/checklist_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d @@ -7861,13 +7819,10 @@ components: type: string checklistItems: description: 'All ChecklistItems that belong to this Checklist.' - example: '["/checklist_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: 'https://example.com/' + format: iri-reference readOnly: true - type: array + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d @@ -8316,7 +8271,7 @@ components: description: '' properties: checklistItems: - description: 'The content types that are most likely to be useful for planning programme of this category.' + description: 'List of selected ChecklistItems.' example: '["/checklist_items/1a2b3c4d"]' items: example: 'https://example.com/' @@ -8564,7 +8519,7 @@ components: - 'null' writeOnly: true checklistItems: - description: 'The content types that are most likely to be useful for planning programme of this category.' + description: 'List of selected ChecklistItems.' example: '["/checklist_items/1a2b3c4d"]' items: example: 'https://example.com/' @@ -8683,7 +8638,7 @@ components: type: object type: object checklistItems: - description: 'The content types that are most likely to be useful for planning programme of this category.' + description: 'List of selected ChecklistItems.' example: '["/checklist_items/1a2b3c4d"]' items: example: 'https://example.com/' @@ -8890,7 +8845,7 @@ components: readOnly: true type: string checklistItems: - description: 'The content types that are most likely to be useful for planning programme of this category.' + description: 'List of selected ChecklistItems.' example: '["/checklist_items/1a2b3c4d"]' items: example: 'https://example.com/' @@ -24319,7 +24274,7 @@ paths: get: deprecated: false description: 'Retrieves the collection of Checklist resources.' - operationId: BelongsToCamp_App\Entity\Checklist_get_collection + operationId: api_camps_campIdchecklists_get_collection parameters: - allowEmptyValue: false @@ -25153,7 +25108,7 @@ paths: get: deprecated: false description: 'Retrieves the collection of ChecklistItem resources.' - operationId: BelongsToChecklist_App\Entity\ChecklistItem_get_collection + operationId: api_checklists_checklistIdchecklist_items_get_collection parameters: - allowEmptyValue: false diff --git a/api/tests/Api/SnapshotTests/__snapshots__/performance_test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/performance_test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml index fdc9790ac3..00d3d68b59 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/performance_test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/performance_test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml @@ -4,10 +4,10 @@ /activity_progress_labels/item: 7 /activity_responsibles: 6 /activity_responsibles/item: 8 -/camps: 39 -/camps/item: 32 -/camp_collaborations: 25 -/camp_collaborations/item: 25 +/camps: 36 +/camps/item: 31 +/camp_collaborations: 22 +/camp_collaborations/item: 24 /categories: 11 /categories/item: 9 /checklists: 6 @@ -25,7 +25,7 @@ /material_lists: 6 /material_lists/item: 7 /periods: 6 -/periods/item: 28 +/periods/item: 27 /profiles: 6 /profiles/item: 6 /schedule_entries: 23 @@ -34,8 +34,8 @@ '/activities?camp=': 213 '/activity_progress_labels?camp=': 6 '/activity_responsibles?activity.camp=': 6 -'/camp_collaborations?camp=': 13 -'/camp_collaborations?activityResponsibles.activity=': 25 +'/camp_collaborations?camp=': 12 +'/camp_collaborations?activityResponsibles.activity=': 24 '/categories?camp=': 9 '/content_types?categories=': 6 '/day_responsibles?day.period=': 6 diff --git a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml index 85260de4e7..31e42033f3 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml @@ -4,10 +4,10 @@ /activity_progress_labels/item: 7 /activity_responsibles: 6 /activity_responsibles/item: 8 -/camps: 29 -/camps/item: 22 -/camp_collaborations: 25 -/camp_collaborations/item: 15 +/camps: 26 +/camps/item: 21 +/camp_collaborations: 22 +/camp_collaborations/item: 14 /categories: 11 /categories/item: 9 /checklists: 6 @@ -25,7 +25,7 @@ /material_lists: 6 /material_lists/item: 7 /periods: 6 -/periods/item: 18 +/periods/item: 17 /profiles: 6 /profiles/item: 6 /schedule_entries: 23 @@ -34,8 +34,8 @@ '/activities?camp=': 13 '/activity_progress_labels?camp=': 6 '/activity_responsibles?activity.camp=': 6 -'/camp_collaborations?camp=': 13 -'/camp_collaborations?activityResponsibles.activity=': 15 +'/camp_collaborations?camp=': 12 +'/camp_collaborations?activityResponsibles.activity=': 14 '/categories?camp=': 9 '/content_types?categories=': 6 '/day_responsibles?day.period=': 6 From bdd8b35a717b7d71c6b4fd7b48033117e353e7c8 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Thu, 8 Aug 2024 19:42:36 +0200 Subject: [PATCH 17/30] UnitTest ChecklistNode can only contain ChecklistItems of the same camp --- api/src/Entity/ContentNode/ChecklistNode.php | 2 + .../ChecklistItem/AssertBelongsToSameCamp.php | 10 ++++ .../AssertBelongsToSameCampValidator.php | 55 +++++++++++++++++++ .../ChecklistNode/UpdateChecklistNodeTest.php | 14 +++++ 4 files changed, 81 insertions(+) create mode 100644 api/src/Validator/ChecklistItem/AssertBelongsToSameCamp.php create mode 100644 api/src/Validator/ChecklistItem/AssertBelongsToSameCampValidator.php diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php index 433be7ec36..2f6dcc8c39 100644 --- a/api/src/Entity/ContentNode/ChecklistNode.php +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -15,6 +15,7 @@ use App\Repository\ChecklistNodeRepository; use App\State\ContentNode\ChecklistNodePersistProcessor; use App\Util\EntityMap; +use App\Validator\ChecklistItem\AssertBelongsToSameCamp; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -62,6 +63,7 @@ class ChecklistNode extends ContentNode { #[ORM\OrderBy(['position' => 'ASC'])] public Collection $checklistItems; + #[AssertBelongsToSameCamp(groups: ['update'])] #[ApiProperty(example: '["1a2b3c4d"]')] #[Groups(['write'])] public ?array $addChecklistItemIds = []; diff --git a/api/src/Validator/ChecklistItem/AssertBelongsToSameCamp.php b/api/src/Validator/ChecklistItem/AssertBelongsToSameCamp.php new file mode 100644 index 0000000000..e27bffc8d6 --- /dev/null +++ b/api/src/Validator/ChecklistItem/AssertBelongsToSameCamp.php @@ -0,0 +1,10 @@ +context->getObject(); + if (!$object instanceof ChecklistNode) { + throw new UnexpectedValueException($object, ChecklistNode::class); + } + + $camp = $this->getCampFromInterface($object, $this->em); + + foreach($value as $checklistItemId) { + /** @var ChecklistItem $checklistItem */ + $checklistItem = $this->checklistItemRepository->find($checklistItemId); + + if ($camp != $checklistItem?->getCamp()) { + $this->context->buildViolation($constraint->message) + ->addViolation(); + } + } + } +} diff --git a/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php b/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php index a2cd67de89..679348df60 100644 --- a/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php +++ b/api/tests/Api/ContentNodes/ChecklistNode/UpdateChecklistNodeTest.php @@ -103,4 +103,18 @@ public function testRemoveChecklistItemForManager() { $checklistNode = $this->getEntityManager()->getRepository(ChecklistNode::class)->find($this->defaultEntity->getId()); $this->assertFalse(in_array($checklistItem, $checklistNode->getChecklistItems())); } + + public function testAddChecklistItemOfOtherCampIsDenied() { + $checklistItemId = static::getFixture('checklistItem2_1_1')->getId(); + static::createClientWithCredentials(['email' => static::getFixture('user2member')->getEmail()]) + ->request('PATCH', $this->endpoint.'/'.$this->defaultEntity->getId(), ['json' => [ + 'addChecklistItemIds' => [$checklistItemId], + ], 'headers' => ['Content-Type' => 'application/merge-patch+json']]) + ; + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'addChecklistItemIds: Must belong to the same camp.', + ]); + } } From 46b5519dec4a493ce5d0a6c07dd5f37c3a59d240 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Thu, 8 Aug 2024 19:45:10 +0200 Subject: [PATCH 18/30] changes according to review --- api/src/Entity/Checklist.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/src/Entity/Checklist.php b/api/src/Entity/Checklist.php index 336177e806..1746c30021 100644 --- a/api/src/Entity/Checklist.php +++ b/api/src/Entity/Checklist.php @@ -35,9 +35,7 @@ security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' ), new Delete( - security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)', - validate: true, - validationContext: ['groups' => ['delete']] + security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' ), new GetCollection( security: 'is_authenticated()' From 630b9abacb8bba6e04f12e34f9b4db9da2cc1fff Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Thu, 8 Aug 2024 20:10:57 +0200 Subject: [PATCH 19/30] UnitTest ChecklistItem NoParentLoop --- api/src/Entity/ChecklistItem.php | 4 ++-- .../UpdateChecklistItemTest.php | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php index 9317985251..4a414832b5 100644 --- a/api/src/Entity/ChecklistItem.php +++ b/api/src/Entity/ChecklistItem.php @@ -83,8 +83,8 @@ class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFr * root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long * as the new parent is in the same checklist as the old one. */ - #[AssertBelongsToSameChecklist(groups: ['update'])] - #[AssertNoLoop(groups: ['update'])] + #[AssertBelongsToSameChecklist] + #[AssertNoLoop] #[ApiProperty(example: '/checklist_items/1a2b3c4d')] #[Gedmo\SortableGroup] #[Groups(['read', 'write'])] diff --git a/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php b/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php index 0152401f68..5237deb0bb 100644 --- a/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php +++ b/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php @@ -110,6 +110,28 @@ public function testPatchChecklistItemDisallowsChangingChecklist() { ]); } + public function testPatchhChecklistItemValidatesNoParentLoop() { + $checklistItemParent = static::getFixture('checklistItem1_1_2'); + $checklistItemChild = static::getFixture('checklistItem1_1_2_3'); + + static::createClientWithCredentials()->request( + 'PATCH', + '/checklist_items/'.$checklistItemParent->getId(), + [ + 'json' => [ + 'parent' => '/checklist_items/'.$checklistItemChild->getId(), + ], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ] + ); + + $this->assertResponseStatusCodeSame(422); + $this->assertJsonContains([ + 'title' => 'An error occurred', + 'detail' => 'parent: Must not form a loop of parent-child relations.', + ]); + } + public function testPatchChecklistItemValidatesNullText() { $checklistItem = static::getFixture('checklistItem1_1_1'); static::createClientWithCredentials()->request( From 5cf86d54d80c2016d44cef5032fa17f25e07f759 Mon Sep 17 00:00:00 2001 From: Pirmin Mattmann Date: Thu, 8 Aug 2024 20:14:43 +0200 Subject: [PATCH 20/30] cs-fix; phpstan --- .../AssertBelongsToSameCampValidator.php | 11 ++++++----- .../Api/ChecklistItems/UpdateChecklistItemTest.php | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/src/Validator/ChecklistItem/AssertBelongsToSameCampValidator.php b/api/src/Validator/ChecklistItem/AssertBelongsToSameCampValidator.php index 7716448fa7..373471ef3b 100644 --- a/api/src/Validator/ChecklistItem/AssertBelongsToSameCampValidator.php +++ b/api/src/Validator/ChecklistItem/AssertBelongsToSameCampValidator.php @@ -15,9 +15,9 @@ class AssertBelongsToSameCampValidator extends ConstraintValidator { use GetCampFromContentNodeTrait; - + public function __construct( - public RequestStack $requestStack, + public RequestStack $requestStack, private EntityManagerInterface $em, private ChecklistItemRepository $checklistItemRepository, ) {} @@ -42,13 +42,14 @@ public function validate($value, Constraint $constraint): void { $camp = $this->getCampFromInterface($object, $this->em); - foreach($value as $checklistItemId) { - /** @var ChecklistItem $checklistItem */ + foreach ($value as $checklistItemId) { + /** @var ?ChecklistItem $checklistItem */ $checklistItem = $this->checklistItemRepository->find($checklistItemId); if ($camp != $checklistItem?->getCamp()) { $this->context->buildViolation($constraint->message) - ->addViolation(); + ->addViolation() + ; } } } diff --git a/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php b/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php index 5237deb0bb..24462d3c0e 100644 --- a/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php +++ b/api/tests/Api/ChecklistItems/UpdateChecklistItemTest.php @@ -124,7 +124,7 @@ public function testPatchhChecklistItemValidatesNoParentLoop() { 'headers' => ['Content-Type' => 'application/merge-patch+json'], ] ); - + $this->assertResponseStatusCodeSame(422); $this->assertJsonContains([ 'title' => 'An error occurred', From 15187f03f3efa50d05dc9550f1fd7cd79c242392 Mon Sep 17 00:00:00 2001 From: Manuel Meister Date: Sun, 30 Jun 2024 19:12:15 +0200 Subject: [PATCH 21/30] Add checklist to frontend admin --- common/locales/de.json | 12 ++ common/locales/en.json | 12 ++ common/locales/fr.json | 11 + .../components/checklist/ChecklistCreate.vue | 83 ++++++++ .../components/checklist/ChecklistItem.vue | 94 ++++++++ .../checklist/ChecklistItemCreate.vue | 82 +++++++ .../checklist/ChecklistItemEdit.vue | 69 ++++++ .../checklist/SortableChecklist.vue | 201 ++++++++++++++++++ frontend/src/locales/de.json | 15 ++ frontend/src/locales/en.json | 15 ++ frontend/src/router.js | 61 ++++++ frontend/src/views/admin/Checklists.vue | 56 +++++ frontend/src/views/admin/SideBarAdmin.vue | 5 + frontend/src/views/checklist/Checklist.vue | 55 +++++ .../src/views/checklist/SideBarChecklist.vue | 29 +++ 15 files changed, 800 insertions(+) create mode 100644 frontend/src/components/checklist/ChecklistCreate.vue create mode 100644 frontend/src/components/checklist/ChecklistItem.vue create mode 100644 frontend/src/components/checklist/ChecklistItemCreate.vue create mode 100644 frontend/src/components/checklist/ChecklistItemEdit.vue create mode 100644 frontend/src/components/checklist/SortableChecklist.vue create mode 100644 frontend/src/views/admin/Checklists.vue create mode 100644 frontend/src/views/checklist/Checklist.vue create mode 100644 frontend/src/views/checklist/SideBarChecklist.vue diff --git a/common/locales/de.json b/common/locales/de.json index c8f3f1d83f..5995eceffb 100644 --- a/common/locales/de.json +++ b/common/locales/de.json @@ -169,6 +169,18 @@ "-": "keine Nummerierung" } }, + "checklist": { + "fields": { + "name": "Name" + }, + "name": "Checkliste | Checklisten" + }, + "checklistItem": { + "fields": { + "text": "Text" + }, + "name": "Checklisteneintrag" + }, "contentType": { "name": "Inhalttyp | Inhalttypen" }, diff --git a/common/locales/en.json b/common/locales/en.json index 1093dd758c..1b918ccd64 100644 --- a/common/locales/en.json +++ b/common/locales/en.json @@ -176,6 +176,18 @@ "-": "no numbering" } }, + "checklist": { + "fields": { + "name": "Name" + }, + "name": "Checklist | Checklists" + }, + "checklistItem": { + "fields": { + "text": "Text" + }, + "name": "Checklist entry" + }, "contentType": { "name": "Contenttype | Contenttypes" }, diff --git a/common/locales/fr.json b/common/locales/fr.json index db1a1a74a4..96f2ba8fdb 100644 --- a/common/locales/fr.json +++ b/common/locales/fr.json @@ -157,6 +157,17 @@ "i": "i, ii, iii - chiffres romains minuscules" } }, + "checklist": { + "fields": { + "name": "Nom" + }, + "name": "Check-list | Check-lists" + }, + "checklistItem": { + "fields": { + "text": "Text" + } + }, "contentType": { "name": "Type de contenu | Types de contenu" }, diff --git a/frontend/src/components/checklist/ChecklistCreate.vue b/frontend/src/components/checklist/ChecklistCreate.vue new file mode 100644 index 0000000000..4a59225efb --- /dev/null +++ b/frontend/src/components/checklist/ChecklistCreate.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/frontend/src/components/checklist/ChecklistItem.vue b/frontend/src/components/checklist/ChecklistItem.vue new file mode 100644 index 0000000000..47596d52e4 --- /dev/null +++ b/frontend/src/components/checklist/ChecklistItem.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/frontend/src/components/checklist/ChecklistItemCreate.vue b/frontend/src/components/checklist/ChecklistItemCreate.vue new file mode 100644 index 0000000000..444f357d23 --- /dev/null +++ b/frontend/src/components/checklist/ChecklistItemCreate.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/components/checklist/ChecklistItemEdit.vue b/frontend/src/components/checklist/ChecklistItemEdit.vue new file mode 100644 index 0000000000..5d00317ffd --- /dev/null +++ b/frontend/src/components/checklist/ChecklistItemEdit.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/frontend/src/components/checklist/SortableChecklist.vue b/frontend/src/components/checklist/SortableChecklist.vue new file mode 100644 index 0000000000..55ad6ccc17 --- /dev/null +++ b/frontend/src/components/checklist/SortableChecklist.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 15e660cce8..5ae5147bb9 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -193,6 +193,21 @@ "title": "Kategorie kopieren & einfügen" } }, + "checklist": { + "checklistCreate": { + "title": "Checkliste erstellen" + }, + "checklistItemCreate": { + "add": "Checklisteneintrag erstellen", + "title": "Checklisteneintrag hinzufügen" + }, + "checklistItemEdit": { + "title": "Checklisteneintrag bearbeiten" + }, + "sortableChecklist": { + "add": "Zu \"{parent}\" hinzufügen" + } + }, "collaborator": { "collaboratorCreate": { "invite": "Einladung verschicken", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 01ac2616c3..da2cd59d19 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -193,6 +193,21 @@ "title": "Copy & paste category" } }, + "checklist": { + "checklistCreate": { + "title": "Create checklist" + }, + "checklistItemCreate": { + "add": "Create checklist entry", + "title": "Add checklist entry" + }, + "checklistItemEdit": { + "title": "Edit checklist entry" + }, + "sortableChecklist": { + "add": "Add to \"{parent}\"" + } + }, "collaborator": { "collaboratorCreate": { "invite": "Send invitation", diff --git a/frontend/src/router.js b/frontend/src/router.js index 650a54777a..ae931cea5d 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -338,6 +338,24 @@ export default new Router({ }), }, }, + { + name: 'admin/checklists/checklist', + path: '/camps/:campId/:campTitle?/admin/checklist/:checklistId/:checklistName?', + components: { + navigation: NavigationCamp, + default: () => import('./views/checklist/Checklist.vue'), + aside: () => import('./views/checklist/SideBarChecklist.vue'), + }, + beforeEnter: all([requireAuth, requireCamp, requireChecklist]), + props: { + navigation: (route) => ({ camp: campFromRoute(route) }), + aside: (route) => ({ camp: campFromRoute(route) }), + default: (route) => ({ + camp: campFromRoute(route), + checklist: checklistFromRoute(route), + }), + }, + }, { path: '/camps/:campId/:campTitle?/admin', components: { @@ -384,6 +402,12 @@ export default new Router({ component: () => import('./views/admin/Print.vue'), props: (route) => ({ camp: campFromRoute(route) }), }, + { + path: 'checklists', + name: 'admin/checklists', + component: () => import('./views/admin/Checklists.vue'), + props: (route) => ({ camp: campFromRoute(route) }), + }, { path: 'materiallists', name: 'camp/material', @@ -567,6 +591,25 @@ async function requireMaterialList(to, from, next) { } } +async function requireChecklist(to, from, next) { + const checklist = await checklistFromRoute(to) + if (checklist === undefined) { + next({ + name: 'PageNotFound', + params: [to.fullPath, ''], + replace: true, + }) + } else { + await checklist._meta.load + .then(() => { + next() + }) + .catch(() => { + next(campRoute(campFromRoute(to))) + }) + } +} + export function campFromRoute(route) { return apiStore.get().camps({ id: route.params.campId }) } @@ -593,6 +636,10 @@ export function materialListFromRoute(route) { return apiStore.get().materialLists({ id: route.params.materialId }) } +export function checklistFromRoute(route) { + return apiStore.get().checklists({ id: route.params.checklistId }) +} + function getContentLayout(route) { switch (route.name) { case 'camp/period/program': @@ -722,6 +769,20 @@ export function categoryRoute(camp, category, query = {}) { } } +export function checklistRoute(camp, checklist, query = {}) { + if (camp._meta.loading || checklist._meta.loading) return {} + return { + name: 'admin/checklists/checklist', + params: { + campId: camp.id, + campTitle: slugify(camp.title), + checklistId: checklist.id, + checklistName: slugify(checklist.name), + }, + query, + } +} + async function firstFuturePeriod(route) { const periods = await apiStore.get().camps({ id: route.params.campId }).periods()._meta .load diff --git a/frontend/src/views/admin/Checklists.vue b/frontend/src/views/admin/Checklists.vue new file mode 100644 index 0000000000..4bba314b0c --- /dev/null +++ b/frontend/src/views/admin/Checklists.vue @@ -0,0 +1,56 @@ + + + diff --git a/frontend/src/views/admin/SideBarAdmin.vue b/frontend/src/views/admin/SideBarAdmin.vue index 2d1abd7e4a..76a4074130 100644 --- a/frontend/src/views/admin/SideBarAdmin.vue +++ b/frontend/src/views/admin/SideBarAdmin.vue @@ -17,6 +17,11 @@ :title="$tc('views.admin.sideBarAdmin.itemCollaborators')" icon="mdi-account-group-outline" /> + + + + + + + + + + + + diff --git a/frontend/src/views/checklist/SideBarChecklist.vue b/frontend/src/views/checklist/SideBarChecklist.vue new file mode 100644 index 0000000000..dc4fa7f6c7 --- /dev/null +++ b/frontend/src/views/checklist/SideBarChecklist.vue @@ -0,0 +1,29 @@ + + + From c4ef98ddfb1944d2a0f39baf2a6a6d3492b047a9 Mon Sep 17 00:00:00 2001 From: Manuel Meister Date: Fri, 5 Jul 2024 21:20:26 +0200 Subject: [PATCH 22/30] Create checklist contentnode --- common/locales/de.json | 4 + .../src/components/activity/ContentNode.vue | 2 + .../components/activity/content/Checklist.vue | 219 ++++++++++++++++++ .../content/checklist/ChecklistItem.vue | 60 +++++ .../checklist/SortableChecklist.vue | 21 +- ...listItem.vue => SortableChecklistItem.vue} | 28 +-- frontend/src/locales/de.json | 3 + frontend/src/locales/en.json | 3 + frontend/src/views/admin/SideBarAdmin.vue | 2 +- 9 files changed, 317 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/activity/content/Checklist.vue create mode 100644 frontend/src/components/activity/content/checklist/ChecklistItem.vue rename frontend/src/components/checklist/{ChecklistItem.vue => SortableChecklistItem.vue} (70%) diff --git a/common/locales/de.json b/common/locales/de.json index 5995eceffb..e3f8c07f0c 100644 --- a/common/locales/de.json +++ b/common/locales/de.json @@ -7,6 +7,10 @@ } }, "contentNode": { + "checklist": { + "icon": "mdi-clipboard-list-outline", + "name": "Checkliste" + }, "columnLayout": { "entity": { "column": { diff --git a/frontend/src/components/activity/ContentNode.vue b/frontend/src/components/activity/ContentNode.vue index 4f152eb231..be53044258 100644 --- a/frontend/src/components/activity/ContentNode.vue +++ b/frontend/src/components/activity/ContentNode.vue @@ -23,6 +23,7 @@ import LearningTopics from './content/LearningTopics.vue' import SafetyConcept from './content/SafetyConcept.vue' import Storyboard from './content/Storyboard.vue' import Storycontext from './content/Storycontext.vue' +import Checklist from './content/Checklist.vue' const contentNodeComponents = { ColumnLayout, @@ -35,6 +36,7 @@ const contentNodeComponents = { SafetyConcept, Storyboard, Storycontext, + Checklist, } export default { diff --git a/frontend/src/components/activity/content/Checklist.vue b/frontend/src/components/activity/content/Checklist.vue new file mode 100644 index 0000000000..074286c895 --- /dev/null +++ b/frontend/src/components/activity/content/Checklist.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/frontend/src/components/activity/content/checklist/ChecklistItem.vue b/frontend/src/components/activity/content/checklist/ChecklistItem.vue new file mode 100644 index 0000000000..8800e15cae --- /dev/null +++ b/frontend/src/components/activity/content/checklist/ChecklistItem.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/frontend/src/components/checklist/SortableChecklist.vue b/frontend/src/components/checklist/SortableChecklist.vue index 55ad6ccc17..aa72a2e498 100644 --- a/frontend/src/components/checklist/SortableChecklist.vue +++ b/frontend/src/components/checklist/SortableChecklist.vue @@ -3,7 +3,7 @@ - -
-

{{ checklist.name }}

-
    - -
+
+ + + +

+ {{ checklist.name }} + + ({{ + selectionContentNode.filter( + (item) => + checkedItems.includes(item.id) && + item.checklist()._meta.self === checklist?._meta?.self + ).length + }} + selected) + +

+
+ +
    + +
+
+
+
@@ -104,35 +122,58 @@ export default { } }, computed: { + campChecklistItems() { + return this.api.get().checklistItems().items + }, selectionContentNode() { - return this.api - .get() - .checklistItems() - .items.filter((item) => - this.contentNode - .checklistItems() - .items.some(({ _meta }) => _meta.self === item._meta.self) - ) + return this.campChecklistItems.filter((item) => + this.contentNode + .checklistItems() + .items.some(({ _meta }) => _meta.self === item._meta.self) + ) }, serverSelection() { return this.selectionContentNode.map((item) => item.id) }, + allChecklists() { + return this.camp.checklists().items.map((checklist) => ({ + checklist, + items: this.campChecklistItems + .filter((item) => item.checklist()._meta.self === checklist?._meta.self) + .map((item) => ({ + item, + parents: this.itemsLoaded ? this.getParents(item) : [], + })) + .sort((a, b) => { + const aparents = [ + ...a.parents.map(({ position }) => position), + a.item.position, + -1, + ] + const bparents = [ + ...b.parents.map(({ position }) => position), + b.item.position, + -1, + ] + for (let i = 0; i < Math.min(aparents.length, bparents.length); i++) { + if (aparents[i] !== bparents[i]) { + return aparents[i] - bparents[i] + } + } + return 0 + }), + })) + }, activeChecklists() { - return this.camp - .checklists() - .items.filter(({ _meta }) => + return this.allChecklists + .filter(({ checklist }) => this.contentNode .checklistItems() - .items.some((item) => _meta.self === item?.checklist()._meta.self) + .items.some((item) => checklist._meta.self === item?.checklist()._meta.self) ) - .map((checklist) => ({ + .map(({ checklist, items }) => ({ checklist, - items: this.selectionContentNode - .filter((item) => item.checklist()._meta.self === checklist._meta.self) - .map((item) => ({ - item, - parents: this.itemsLoaded ? this.getParents(item) : [], - })), + items: items.filter(({ item }) => this.checkedItems.includes(item.id)), })) }, }, diff --git a/frontend/src/components/checklist/ChecklistItemCreate.vue b/frontend/src/components/checklist/ChecklistItemCreate.vue index 444f357d23..d8cf07b42e 100644 --- a/frontend/src/components/checklist/ChecklistItemCreate.vue +++ b/frontend/src/components/checklist/ChecklistItemCreate.vue @@ -24,6 +24,7 @@ type="text" path="text" vee-rules="required" + autofocus /> diff --git a/frontend/src/components/checklist/ChecklistItemEdit.vue b/frontend/src/components/checklist/ChecklistItemEdit.vue index 5d00317ffd..4bf61d5488 100644 --- a/frontend/src/components/checklist/ChecklistItemEdit.vue +++ b/frontend/src/components/checklist/ChecklistItemEdit.vue @@ -20,6 +20,7 @@ type="text" path="text" vee-rules="required" + autofocus /> diff --git a/frontend/src/components/checklist/SortableChecklist.vue b/frontend/src/components/checklist/SortableChecklist.vue index aa72a2e498..5af2d61bef 100644 --- a/frontend/src/components/checklist/SortableChecklist.vue +++ b/frontend/src/components/checklist/SortableChecklist.vue @@ -147,8 +147,7 @@ export default { // patch content node location await this.api .patch(event.item.dataset.href, { - position: - event.newDraggableIndex + (event.from === event.to || !parent ? 1 : 0), + position: event.newDraggableIndex, parent, }) .catch((e) => { diff --git a/frontend/src/views/checklist/Checklist.vue b/frontend/src/views/checklist/Checklist.vue index 15dee209fd..ad3c60da07 100644 --- a/frontend/src/views/checklist/Checklist.vue +++ b/frontend/src/views/checklist/Checklist.vue @@ -2,6 +2,7 @@ Date: Tue, 27 Aug 2024 22:19:11 +0200 Subject: [PATCH 24/30] Add feature toggle for checklist --- .github/workflows/reusable-dev-deployment.yml | 1 + .helm/deploy-to-cluster.sh | 1 + .../ecamp3/templates/frontend_configmap.yaml | 1 + .helm/ecamp3/values.yaml | 3 +- .../activity/ButtonNestedContentNodeAdd.vue | 9 ++- frontend/src/environment.js | 1 + frontend/src/router.js | 58 +++++++++++-------- frontend/src/views/admin/SideBarAdmin.vue | 7 +++ 8 files changed, 55 insertions(+), 26 deletions(-) diff --git a/.github/workflows/reusable-dev-deployment.yml b/.github/workflows/reusable-dev-deployment.yml index d0f3ae3abd..456513e1e1 100644 --- a/.github/workflows/reusable-dev-deployment.yml +++ b/.github/workflows/reusable-dev-deployment.yml @@ -130,6 +130,7 @@ jobs: --set recaptcha.secret='${{ secrets.RECAPTCHA_SECRET }}' \ --set frontend.loginInfoTextKey=${{ vars.LOGIN_INFO_TEXT_KEY }} \ --set featureToggle.developer=true + --set featureToggle.checklist=true - name: Finish the GitHub deployment uses: bobheadxi/deployments@v1.5.0 diff --git a/.helm/deploy-to-cluster.sh b/.helm/deploy-to-cluster.sh index fcf7c17def..4702dbe0ed 100755 --- a/.helm/deploy-to-cluster.sh +++ b/.helm/deploy-to-cluster.sh @@ -64,6 +64,7 @@ for i in 1; do values="$values --set deploymentTime=$(date -u +%s)" values="$values --set deployedVersion=\"$(git rev-parse --short HEAD)\"" values="$values --set featureToggle.developer=true" + values="$values --set featureToggle.checklist=true" if [ -n "$BACKUP_SCHEDULE" ]; then values="$values --set postgresql.backup.schedule=$BACKUP_SCHEDULE" diff --git a/.helm/ecamp3/templates/frontend_configmap.yaml b/.helm/ecamp3/templates/frontend_configmap.yaml index 4e18b26ed7..d58504aa32 100644 --- a/.helm/ecamp3/templates/frontend_configmap.yaml +++ b/.helm/ecamp3/templates/frontend_configmap.yaml @@ -29,6 +29,7 @@ data: RECAPTCHA_SITE_KEY: null, {{- end }} FEATURE_DEVELOPER: {{ .Values.featureToggle.developer | default false }}, + FEATURE_CHECKLIST: {{ .Values.featureToggle.checklist | default false }}, LOGIN_INFO_TEXT_KEY: '{{ .Values.frontend.loginInfoTextKey }}', } deployedVersion: {{ .Values.deployedVersion | quote }} diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index e60ab9fcb2..3a11d75bd9 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -15,6 +15,7 @@ helpLink: # 'https://ecamp3.ch/faq' # enable/disable feature across the complete deployment featureToggle: developer: false # enables various tools/features foreseen for development deployments (language switcher, form controls view, performance measurement view, etc.) + checklist: false # enables various tools/features foreseen for development deployments (language switcher, form controls view, performance measurement view, etc.) api: subpath: "/api" @@ -251,7 +252,7 @@ apiCache: requests: cpu: 10m memory: 20Mi - + autoscaling: enabled: false minReplicas: 1 diff --git a/frontend/src/components/activity/ButtonNestedContentNodeAdd.vue b/frontend/src/components/activity/ButtonNestedContentNodeAdd.vue index 6fb3ef9535..9013c978aa 100644 --- a/frontend/src/components/activity/ButtonNestedContentNodeAdd.vue +++ b/frontend/src/components/activity/ButtonNestedContentNodeAdd.vue @@ -60,6 +60,7 @@ From a8303d90364251f4887b4a0d4436ac8e3f50b8c5 Mon Sep 17 00:00:00 2001 From: Manuel Meister Date: Thu, 12 Sep 2024 21:09:50 +0200 Subject: [PATCH 25/30] Combine contentType filter --- .../activity/ButtonNestedContentNodeAdd.vue | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/activity/ButtonNestedContentNodeAdd.vue b/frontend/src/components/activity/ButtonNestedContentNodeAdd.vue index 9013c978aa..2cc05ed505 100644 --- a/frontend/src/components/activity/ButtonNestedContentNodeAdd.vue +++ b/frontend/src/components/activity/ButtonNestedContentNodeAdd.vue @@ -83,7 +83,7 @@ export default { return [] } return this.preferredContentTypes() - .items.filter((ct) => this.showResponsiveLayout(ct) && this.showChecklistNode(ct)) + .items.filter(this.filterContentType) .sort(this.sortContentTypeByTranslatedName) }, nonpreferredContentTypesItems() { @@ -95,7 +95,7 @@ export default { .contentTypes() .items.filter( (ct) => - this.showResponsiveLayout(ct) && + this.filterContentType(ct) && !this.preferredContentTypes() .items.map((ct) => ct.id) .includes(ct.id) @@ -116,13 +116,15 @@ export default { contentTypeIconKey(contentType) { return 'contentNode.' + camelCase(contentType.name) + '.icon' }, - showResponsiveLayout(contentType) { - return ( - contentType.name !== 'ResponsiveLayout' || this.parentContentNode.parent === null - ) - }, - showChecklistNode(contentType) { - return contentType.name === 'Checklist' ? this.featureChecklistEnabled : true + filterContentType(contentType) { + switch(contentType.name) { + case 'ResponsiveLayout': + return this.parentContentNode.parent === null + case 'Checklist': + return this.featureChecklistEnabled + default: + return true + } }, sortContentTypeByTranslatedName(ct1, ct2) { const ct1name = this.$i18n.tc(this.contentTypeNameKey(ct1)) From 45057f52d6e280794d855d78dc30a4b9e6ad0694 Mon Sep 17 00:00:00 2001 From: Manuel Meister Date: Thu, 12 Sep 2024 21:10:36 +0200 Subject: [PATCH 26/30] Fix description of checklist featureToggle --- .helm/ecamp3/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index 3a11d75bd9..504272c251 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -15,7 +15,7 @@ helpLink: # 'https://ecamp3.ch/faq' # enable/disable feature across the complete deployment featureToggle: developer: false # enables various tools/features foreseen for development deployments (language switcher, form controls view, performance measurement view, etc.) - checklist: false # enables various tools/features foreseen for development deployments (language switcher, form controls view, performance measurement view, etc.) + checklist: false # enables checklist feature in frontend api: subpath: "/api" From 46651ece449c033f786ea77d4e73e478adf723dd Mon Sep 17 00:00:00 2001 From: Manuel Meister Date: Thu, 12 Sep 2024 22:29:20 +0200 Subject: [PATCH 27/30] Add content if empty --- .../components/activity/content/Checklist.vue | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/activity/content/Checklist.vue b/frontend/src/components/activity/content/Checklist.vue index 03bc49e8e9..05c029e26d 100644 --- a/frontend/src/components/activity/content/Checklist.vue +++ b/frontend/src/components/activity/content/Checklist.vue @@ -8,20 +8,21 @@ >