Skip to content

Commit 4233aee

Browse files
authored
Merge pull request #7217 from pmattmann/feature/materialitem-nullable
MaterialItem.MaterialList is nullable
2 parents 659dfc6 + 56f99e6 commit 4233aee

27 files changed

+257
-52
lines changed

Diff for: api/migrations/schema/Version20250412165829.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Auto-generated Migration: Please modify to your needs!
12+
*/
13+
final class Version20250412165829 extends AbstractMigration {
14+
public function getDescription(): string {
15+
return 'MaterialItem.MaterialList is nullable';
16+
}
17+
18+
public function up(Schema $schema): void {
19+
// this up() migration is auto-generated, please modify it to your needs
20+
$this->addSql(<<<'SQL'
21+
ALTER TABLE material_item ALTER materialListId DROP NOT NULL
22+
SQL);
23+
}
24+
25+
public function down(Schema $schema): void {
26+
// this down() migration is auto-generated, please modify it to your needs
27+
$this->addSql(<<<'SQL'
28+
ALTER TABLE material_item ALTER materiallistid SET NOT NULL
29+
SQL);
30+
}
31+
}

Diff for: api/src/Entity/ContentNode.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
#[ORM\InheritanceType('SINGLE_TABLE')]
5454
#[ORM\DiscriminatorColumn(name: 'strategy', type: 'string')]
5555
#[ORM\UniqueConstraint(name: 'contentnode_parentid_slot_position_unique', columns: ['parentid', 'slot', 'position'])]
56-
abstract class ContentNode extends BaseEntity implements BelongsToContentNodeTreeInterface, CopyFromPrototypeInterface, HasParentInterface {
56+
abstract class ContentNode extends BaseEntity implements BelongsToCampInterface, BelongsToContentNodeTreeInterface, CopyFromPrototypeInterface, HasParentInterface {
5757
use ClassInfoTrait;
5858

5959
/**
@@ -193,6 +193,17 @@ public function setParent(?ContentNode $parent) {
193193
$this->root ??= $parent?->root;
194194
}
195195

196+
public function getCamp(): ?Camp {
197+
if (null == $this->getRoot()) {
198+
return null;
199+
}
200+
if ($this->getRoot()->campRootContentNodes->count() > 0) {
201+
return $this->getRoot()->campRootContentNodes[0]->camp;
202+
}
203+
204+
return null;
205+
}
206+
196207
/**
197208
* Holds the actual data of the content node.
198209
*/

Diff for: api/src/Entity/MaterialItem.php

+19-7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Doctrine\Common\Collections\Collection;
2424
use Doctrine\ORM\Mapping as ORM;
2525
use Symfony\Component\Serializer\Annotation\Groups;
26+
use Symfony\Component\Serializer\Attribute\SerializedName;
2627
use Symfony\Component\Validator\Constraints as Assert;
2728

2829
/**
@@ -44,6 +45,7 @@
4445
security: 'is_authenticated()'
4546
),
4647
new Post(
48+
denormalizationContext: ['groups' => ['write', 'create']],
4749
securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object) or object.materialList === null'
4850
),
4951
],
@@ -58,11 +60,12 @@ class MaterialItem extends BaseEntity implements BelongsToCampInterface, CopyFro
5860
* The list to which this item belongs. Lists are used to keep track of who is
5961
* responsible to prepare and bring the item to the camp.
6062
*/
61-
#[AssertBelongsToSameCamp(compareToPrevious: true, groups: ['update'])]
63+
#[Assert\NotNull]
64+
#[AssertBelongsToSameCamp]
6265
#[ApiProperty(example: '/material_lists/1a2b3c4d')]
6366
#[Groups(['read', 'write'])]
6467
#[ORM\ManyToOne(targetEntity: MaterialList::class, inversedBy: 'materialItems')]
65-
#[ORM\JoinColumn(nullable: false, onDelete: 'cascade')]
68+
#[ORM\JoinColumn(nullable: true, onDelete: 'cascade')]
6669
public ?MaterialList $materialList = null;
6770

6871
/**
@@ -131,9 +134,13 @@ public function __construct() {
131134
$this->periodMaterialItems = new ArrayCollection();
132135
}
133136

134-
#[ApiProperty(readable: false)]
137+
#[ApiProperty]
138+
#[SerializedName('camp')]
139+
#[Groups(['read'])]
135140
public function getCamp(): ?Camp {
136-
return $this->materialList?->getCamp();
141+
return $this->period?->getCamp()
142+
?? $this->materialNode?->getCamp()
143+
?? $this->materialList?->getCamp();
137144
}
138145

139146
/**
@@ -143,9 +150,14 @@ public function getCamp(): ?Camp {
143150
public function copyFromPrototype($prototype, $entityMap): void {
144151
$entityMap->add($prototype, $this);
145152

146-
/** @var MaterialList $materialList */
147-
$materialList = $entityMap->get($prototype->materialList);
148-
$materialList->addMaterialItem($this);
153+
if (null != $prototype->materialList) {
154+
/** @var MaterialList $materialList */
155+
$materialList = $entityMap->get($prototype->materialList);
156+
157+
if ($entityMap->belongsToTargetCamp($materialList)) {
158+
$materialList->addMaterialItem($this);
159+
}
160+
}
149161

150162
$this->article = $prototype->article;
151163
$this->quantity = $prototype->quantity;

Diff for: api/src/Repository/MaterialItemRepository.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public function __construct(ManagerRegistry $registry) {
2424
}
2525

2626
public function filterByUser(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, User $user): void {
27-
$materialList = QueryBuilderHelper::findOrAddInnerRootJoinAlias($queryBuilder, $queryNameGenerator, 'materialList');
28-
$this->filterByCampCollaboration($queryBuilder, $user, "{$materialList}.camp");
27+
$periodMaterialItems = QueryBuilderHelper::findOrAddInnerRootJoinAlias($queryBuilder, $queryNameGenerator, 'periodMaterialItems');
28+
$period = QueryBuilderHelper::findOrAddInnerJoinAlias($queryBuilder, $queryNameGenerator, $periodMaterialItems, 'period');
29+
$this->filterByCampCollaboration($queryBuilder, $user, "{$period}.camp");
2930
}
3031
}

Diff for: api/src/State/ActivityCreateProcessor.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public function onBefore($data, Operation $operation, array $uriVariables = [],
3434
throw new \UnexpectedValueException('Property rootContentNode of provided category is of wrong type. Object of type '.ColumnLayout::class.' expected.');
3535
}
3636

37+
$targetCamp = $data->category->camp;
3738
$data->camp = $data->category->camp;
3839
$rootContentNodePrototype = $data->category->rootContentNode;
3940

@@ -50,7 +51,7 @@ public function onBefore($data, Operation $operation, array $uriVariables = [],
5051
$data->setRootContentNode($rootContentNode);
5152

5253
// deep copy from category root node
53-
$entityMap = new EntityMap();
54+
$entityMap = new EntityMap($targetCamp);
5455
$rootContentNode->copyFromPrototype($rootContentNodePrototype, $entityMap);
5556

5657
return $data;

Diff for: api/src/State/CampCreateProcessor.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function onBefore($data, Operation $operation, array $uriVariables = [],
3737

3838
// copy from prototype, if given
3939
if (isset($data->campPrototype)) {
40-
$entityMap = new EntityMap();
40+
$entityMap = new EntityMap($data);
4141
$data->copyFromPrototype($data->campPrototype, $entityMap);
4242
}
4343

Diff for: api/src/State/CategoryCreateProcessor.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function onBefore($data, Operation $operation, array $uriVariables = [],
3838

3939
if (isset($data->copyCategorySource)) {
4040
// CopyActivity Source is set -> copy it's content (rootContentNode)
41-
$entityMap = new EntityMap();
41+
$entityMap = new EntityMap($data->camp);
4242
$rootContentNode->copyFromPrototype($data->copyCategorySource->getRootContentNode(), $entityMap);
4343
}
4444

Diff for: api/src/State/ChecklistCreateProcessor.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function __construct(ProcessorInterface $decorated) {
2222
public function onBefore($data, Operation $operation, array $uriVariables = [], array $context = []): Checklist {
2323
if (isset($data->copyChecklistSource)) {
2424
// CopyChecklist Source is set -> copy it's content
25-
$entityMap = new EntityMap();
25+
$entityMap = new EntityMap($data->camp);
2626
$data->copyFromPrototype($data->copyChecklistSource, $entityMap);
2727
}
2828

Diff for: api/src/Util/EntityMap.php

+8
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
namespace App\Util;
44

55
use App\Entity\BaseEntity;
6+
use App\Entity\BelongsToCampInterface;
7+
use App\Entity\Camp;
68

79
class EntityMap {
810
use ClassInfoTrait;
911

1012
private $map = [];
1113

14+
public function __construct(private Camp $targetCamp) {}
15+
1216
public function add(BaseEntity $prototype, BaseEntity $entity) {
1317
$key = $this->getObjectClass($prototype).'#'.$prototype->getId();
1418
$this->map[$key] = $entity;
@@ -20,4 +24,8 @@ public function get(BaseEntity $prototype): BaseEntity {
2024

2125
return $keyExists ? $this->map[$key] : $prototype;
2226
}
27+
28+
public function belongsToTargetCamp(BelongsToCampInterface $entity) {
29+
return $entity->getCamp() == $this->targetCamp;
30+
}
2331
}

Diff for: api/tests/Api/Activities/CreateActivityTest.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ public function testCreateActivityFromCopySourceWithinSameCamp() {
519519
}
520520

521521
public function testCreateActivityFromCopySourceAcrossCamp() {
522-
static::createClientWithCredentials()->request(
522+
$response = static::createClientWithCredentials()->request(
523523
'POST',
524524
'/activities',
525525
['json' => $this->getExampleWritePayload(
@@ -540,6 +540,21 @@ public function testCreateActivityFromCopySourceAcrossCamp() {
540540

541541
// Activity created
542542
$this->assertResponseStatusCodeSame(201);
543+
544+
// Embedded MaterialNode -> MaterialItems
545+
// Test MaterialList is nulled
546+
$responseArray = $response->toArray();
547+
$contentNodes = $responseArray['_embedded']['contentNodes'];
548+
549+
$materialNodes = array_filter($contentNodes, fn ($cn) => 'Material' == $cn['contentTypeName']);
550+
$this->assertCount(1, $materialNodes);
551+
552+
$materialNode = reset($materialNodes);
553+
$materialItems = $materialNode['_embedded']['materialItems'];
554+
$this->assertCount(1, $materialItems);
555+
556+
$materailItem = reset($materialItems);
557+
$this->assertNull($materailItem['_links']['materialList']);
543558
}
544559

545560
/**

Diff for: api/tests/Api/MaterialItems/CreateMaterialItemTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ public function testCreateMaterialItemValidatesPeriodFromDifferentCamp() {
205205
$this->assertJsonContains([
206206
'violations' => [
207207
[
208-
'propertyPath' => 'period',
208+
'propertyPath' => 'materialList',
209209
'message' => 'Must belong to the same camp.',
210210
],
211211
],
@@ -222,7 +222,7 @@ public function testCreateMaterialItemValidatesMaterialNodeFromDifferentCamp() {
222222
$this->assertJsonContains([
223223
'violations' => [
224224
[
225-
'propertyPath' => 'materialNode',
225+
'propertyPath' => 'materialList',
226226
'message' => 'Must belong to the same camp.',
227227
],
228228
],

Diff for: api/tests/Api/MaterialItems/UpdateMaterialItemTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ public function testPatchMaterialItemValidatesMissingMaterialList() {
151151
'materialList' => null,
152152
], 'headers' => ['Content-Type' => 'application/merge-patch+json']]);
153153

154-
$this->assertResponseStatusCodeSame(400);
154+
$this->assertResponseStatusCodeSame(422);
155155
$this->assertJsonContains([
156-
'detail' => 'The type of the "materialList" attribute must be "array" (nested document) or "string" (IRI), "NULL" given.',
156+
'detail' => 'materialList: This value should not be null.',
157157
]);
158158
}
159159

@@ -244,7 +244,7 @@ public function testPatchMaterialItemValidatesPeriodFromDifferentCamp() {
244244
$this->assertJsonContains([
245245
'violations' => [
246246
[
247-
'propertyPath' => 'period',
247+
'propertyPath' => 'materialList',
248248
'message' => 'Must belong to the same camp.',
249249
],
250250
],
@@ -261,7 +261,7 @@ public function testPatchMaterialItemValidatesMaterialNodeFromDifferentCamp() {
261261
$this->assertJsonContains([
262262
'violations' => [
263263
[
264-
'propertyPath' => 'materialNode',
264+
'propertyPath' => 'materialList',
265265
'message' => 'Must belong to the same camp.',
266266
],
267267
],

Diff for: api/tests/Api/SnapshotTests/EndpointPerformanceTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,9 @@ protected function getSnapshotId(): string {
186186

187187
private static function getContentNodeEndpointQueryCountRanges(): array {
188188
return [
189-
'/content_nodes' => [8, 11],
189+
'/content_nodes' => [10, 12],
190190
'/content_node/column_layouts' => [6, 6],
191-
'/content_node/column_layouts/item' => [10, 10],
191+
'/content_node/column_layouts/item' => [9, 9],
192192
'/content_node/checklist_nodes' => [6, 7],
193193
'/content_node/checklist_nodes/item' => [9, 9],
194194
'/content_node/material_nodes' => [6, 7],

Diff for: api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodematerial_nodes__1.json

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
"materialItems": [
3636
{
3737
"_links": {
38+
"camp": {
39+
"href": "escaped_value"
40+
},
3841
"materialList": {
3942
"href": "escaped_value"
4043
},

Diff for: api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set content_nodes__1.json

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
"materialItems": [
3636
{
3737
"_links": {
38+
"camp": {
39+
"href": "escaped_value"
40+
},
3841
"materialList": {
3942
"href": "escaped_value"
4043
},

Diff for: api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set material_items__1.json

+12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"items": [
44
{
55
"_links": {
6+
"camp": {
7+
"href": "escaped_value"
8+
},
69
"materialList": {
710
"href": "escaped_value"
811
},
@@ -21,6 +24,9 @@
2124
},
2225
{
2326
"_links": {
27+
"camp": {
28+
"href": "escaped_value"
29+
},
2430
"materialList": {
2531
"href": "escaped_value"
2632
},
@@ -39,6 +45,9 @@
3945
},
4046
{
4147
"_links": {
48+
"camp": {
49+
"href": "escaped_value"
50+
},
4251
"materialList": {
4352
"href": "escaped_value"
4453
},
@@ -57,6 +66,9 @@
5766
},
5867
{
5968
"_links": {
69+
"camp": {
70+
"href": "escaped_value"
71+
},
6072
"materialList": {
6173
"href": "escaped_value"
6274
},

Diff for: api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set activities__1.json

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
"materialItems": [
4747
{
4848
"_links": {
49+
"camp": {
50+
"href": "escaped_value"
51+
},
4952
"materialList": {
5053
"href": "escaped_value"
5154
},

Diff for: api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set material_items__1.json

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"_links": {
3+
"camp": {
4+
"href": "escaped_value"
5+
},
36
"materialList": {
47
"href": "escaped_value"
58
},

0 commit comments

Comments
 (0)