Skip to content

Commit 89978e8

Browse files
Merge pull request #2194 from usu/feature/multi-select
API implementation for MultiSelect
2 parents 4df758f + 6ed95ff commit 89978e8

28 files changed

+934
-17
lines changed

api/config/services.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ services:
4848
App\DataPersister\ContentNode\StoryboardDataPersister:
4949
arguments: [ '@api_platform.doctrine.orm.data_persister' ]
5050

51+
App\DataPersister\ContentNode\MultiSelectDataPersister:
52+
arguments: [ '@api_platform.doctrine.orm.data_persister' ]
53+
5154
App\DataPersister\Util\DataPersisterObservable:
5255
arguments: [ '@api_platform.doctrine.orm.data_persister' ]
5356

api/fixtures/contentTypes.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ App\Entity\ContentType:
2727
name: 'LAThematicArea'
2828
active: true
2929
entityClass: 'App\Entity\ContentNode\MultiSelect'
30-
jsonConfig: { items: [ 'outdoorTechnique', 'security', 'natureAndEnvironment', 'pioneeringTechnique', 'campsiteAndSurroundings', 'preventionAndIntegration' ] }
30+
jsonConfig: { items: [ 'option1', 'option2', 'option3' ] }

api/fixtures/multiSelects.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
App\Entity\ContentNode\MultiSelect:
2+
multiSelect1:
3+
root: '@columnLayout1'
4+
parent: '@columnLayout1'
5+
slot: '1'
6+
position: 0
7+
instanceName: <word()>
8+
contentType: '@contentTypeMultiSelect'
9+
multiSelect2:
10+
root: '@columnLayout1'
11+
parent: '@columnLayoutChild1'
12+
slot: '1'
13+
position: 0
14+
instanceName: <word()>
15+
contentType: '@contentTypeMultiSelect'
16+
multiSelectCampUnrelated:
17+
root: '@columnLayout1campUnrelated'
18+
parent: '@columnLayout1campUnrelated'
19+
slot: '1'
20+
position: 1
21+
instanceName: <word()>
22+
contentType: '@contentTypeMultiSelect'
23+
24+
App\Entity\ContentNode\MultiSelectOption:
25+
multiSelectOption1:
26+
multiSelect: '@multiSelect1'
27+
translateKey: <word()>
28+
checked: true
29+
pos: 0
30+
multiSelectOption2:
31+
multiSelect: '@multiSelect2'
32+
translateKey: <word()>
33+
checked: true
34+
pos: 0
35+
multiSelectOptionCampUnrelated:
36+
multiSelect: '@multiSelectCampUnrelated'
37+
translateKey: <word()>
38+
checked: true
39+
pos: 0
40+

api/fixtures/storyboards.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@ App\Entity\ContentNode\StoryboardSection:
2727
column1: <word()>
2828
column2: <word()>
2929
column3: <word()>
30+
pos: 0
3031
storyboardSection2:
3132
storyboard: '@storyboard2'
3233
column1: <word()>
3334
column2: <word()>
3435
column3: <word()>
36+
pos: 0
3537
storyboardSectionCampUnrelated:
3638
storyboard: '@storyboardCampUnrelated'
3739
column1: <word()>
3840
column2: <word()>
3941
column3: <word()>
42+
pos: 0
4043

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\DataPersister\ContentNode;
4+
5+
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
6+
use App\Entity\ContentNode\MultiSelect;
7+
use App\Entity\ContentNode\MultiSelectOption;
8+
9+
class MultiSelectDataPersister extends ContentNodeBaseDataPersister implements ContextAwareDataPersisterInterface {
10+
public function supports($multiSelect, array $context = []): bool {
11+
return ($multiSelect instanceof MultiSelect) && $this->dataPersister->supports($multiSelect, $context);
12+
}
13+
14+
/**
15+
* @param MultiSelect $multiSelect
16+
*/
17+
public function onCreate($multiSelect) {
18+
if (isset($multiSelect->prototype)) {
19+
// copy from Prototype
20+
21+
if (!($multiSelect->prototype instanceof MultiSelect)) {
22+
throw new \Exception('Prototype must be of type MultiSelect');
23+
}
24+
25+
/** @var MultiSelect $prototype */
26+
$prototype = $multiSelect->prototype;
27+
28+
// copy all multiSelect options
29+
foreach ($prototype->options as $prototypeOption) {
30+
$option = new MultiSelectOption();
31+
32+
$option->translateKey = $prototypeOption->translateKey;
33+
$option->checked = $prototypeOption->checked;
34+
$option->setPos($prototypeOption->getPos());
35+
36+
$multiSelect->addOption($option);
37+
}
38+
} else {
39+
// no prototype given --> copy from ContentType config
40+
41+
foreach ($multiSelect->contentType->jsonConfig['items'] as $key => $item) {
42+
$option = new MultiSelectOption();
43+
44+
$option->translateKey = $item;
45+
$option->setPos($key);
46+
47+
$multiSelect->addOption($option);
48+
}
49+
}
50+
51+
parent::onCreate($multiSelect);
52+
}
53+
}

api/src/DataPersister/ContentNode/StoryboardDataPersister.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function onCreate($storyboard) {
3030
$section->column1 = $prototypeSection->column1;
3131
$section->column2 = $prototypeSection->column2;
3232
$section->column3 = $prototypeSection->column3;
33+
$section->setPos($prototypeSection->getPos());
3334

3435
$storyboard->addSection($section);
3536
}

api/src/Entity/ContentNode/MultiSelect.php

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,77 @@
55
use ApiPlatform\Core\Annotation\ApiProperty;
66
use ApiPlatform\Core\Annotation\ApiResource;
77
use App\Entity\ContentNode;
8+
use App\Repository\MultiSelectRepository;
89
use Doctrine\Common\Collections\ArrayCollection;
910
use Doctrine\Common\Collections\Collection;
1011
use Doctrine\ORM\Mapping as ORM;
12+
use Symfony\Component\Serializer\Annotation\Groups;
1113

1214
/**
13-
* @ORM\Entity
15+
* @ORM\Entity(repositoryClass=MultiSelectRepository::class)
1416
* @ORM\Table(name="content_node_multiselect")
15-
* @ApiResource(routePrefix="/content_node")]
1617
*/
18+
#[ApiResource(
19+
routePrefix: '/content_node',
20+
collectionOperations: [
21+
'get' => [
22+
'security' => 'is_fully_authenticated()',
23+
],
24+
'post' => [
25+
'denormalization_context' => ['groups' => ['write', 'create']],
26+
'security_post_denormalize' => 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)',
27+
'validation_groups' => ['Default', 'create'],
28+
],
29+
],
30+
itemOperations: [
31+
'get' => ['security' => 'is_granted("CAMP_COLLABORATOR", object) or is_granted("CAMP_IS_PROTOTYPE", object)'],
32+
'patch' => [
33+
'denormalization_context' => ['groups' => ['write', 'update']],
34+
'security' => 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)',
35+
'validation_groups' => ['Default', 'update'],
36+
],
37+
'delete' => ['security' => '(is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)) and object.owner === null'], // disallow delete when contentNode is a root node
38+
],
39+
denormalizationContext: ['groups' => ['write']],
40+
normalizationContext: ['groups' => ['read']],
41+
)]
1742
class MultiSelect extends ContentNode {
1843
/**
1944
* @ORM\OneToMany(targetEntity="MultiSelectOption", mappedBy="multiSelect", orphanRemoval=true, cascade={"persist"})
2045
*/
2146
#[ApiProperty(readableLink: true, writableLink: false)]
47+
#[Groups(['read'])]
2248
public Collection $options;
2349

2450
public function __construct() {
2551
$this->options = new ArrayCollection();
2652

2753
parent::__construct();
2854
}
55+
56+
/**
57+
* @return MultiSelectOption[]
58+
*/
59+
public function getOptions(): array {
60+
return $this->options->getValues();
61+
}
62+
63+
public function addOption(MultiSelectOption $option): self {
64+
if (!$this->options->contains($option)) {
65+
$this->options->add($option);
66+
$option->multiSelect = $this;
67+
}
68+
69+
return $this;
70+
}
71+
72+
public function removeOption(MultiSelectOption $option): self {
73+
if ($this->options->removeElement($option)) {
74+
if ($option->multiSelect === $this) {
75+
$option->multiSelect = null;
76+
}
77+
}
78+
79+
return $this;
80+
}
2981
}

api/src/Entity/ContentNode/MultiSelectOption.php

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,66 @@
22

33
namespace App\Entity\ContentNode;
44

5+
use ApiPlatform\Core\Annotation\ApiFilter;
56
use ApiPlatform\Core\Annotation\ApiProperty;
67
use ApiPlatform\Core\Annotation\ApiResource;
8+
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
79
use App\Entity\BaseEntity;
10+
use App\Entity\BelongsToCampInterface;
11+
use App\Entity\Camp;
812
use App\Entity\SortableEntityInterface;
913
use App\Entity\SortableEntityTrait;
14+
use App\Repository\MultiSelectOptionRepository;
1015
use Doctrine\ORM\Mapping as ORM;
16+
use Symfony\Component\Serializer\Annotation\Groups;
1117

1218
/**
13-
* @ORM\Entity
19+
* @ORM\Entity(repositoryClass=MultiSelectOptionRepository::class)
1420
* @ORM\Table(name="content_node_multiselect_option")
15-
* @ApiResource(routePrefix="/content_node")]
1621
*/
17-
class MultiSelectOption extends BaseEntity implements SortableEntityInterface {
22+
#[ApiResource(
23+
routePrefix: '/content_node',
24+
collectionOperations: [
25+
'get' => [
26+
'security' => 'is_fully_authenticated()',
27+
],
28+
],
29+
itemOperations: [
30+
'get' => ['security' => 'is_granted("CAMP_COLLABORATOR", object) or is_granted("CAMP_IS_PROTOTYPE", object)'],
31+
'patch' => [
32+
'denormalization_context' => ['groups' => ['write', 'update']],
33+
'security' => 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)',
34+
],
35+
],
36+
denormalizationContext: ['groups' => ['write']],
37+
normalizationContext: ['groups' => ['read']],
38+
)]
39+
#[ApiFilter(SearchFilter::class, properties: ['multiSelect'])]
40+
class MultiSelectOption extends BaseEntity implements BelongsToCampInterface, SortableEntityInterface {
1841
use SortableEntityTrait;
1942

2043
/**
2144
* @ORM\ManyToOne(targetEntity="MultiSelect", inversedBy="options")
2245
* @ORM\JoinColumn(nullable=false, onDelete="cascade")
2346
*/
2447
#[ApiProperty(readableLink: false, writableLink: false)]
48+
#[Groups(['read'])]
2549
public MultiSelect $multiSelect;
2650

2751
/**
2852
* @ORM\Column(type="text", nullable=false)
2953
*/
54+
#[Groups(['read'])]
3055
public string $translateKey;
3156

3257
/**
3358
* @ORM\Column(type="boolean", nullable=false)
3459
*/
60+
#[Groups(['read', 'update'])]
3561
public bool $checked = false;
62+
63+
#[ApiProperty(readable: false)]
64+
public function getCamp(): ?Camp {
65+
return $this->multiSelect?->getCamp();
66+
}
3667
}

api/src/Entity/ContentNode/Storyboard.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
)]
4242
class Storyboard extends ContentNode {
4343
/**
44-
* @ORM\OneToMany(targetEntity="StoryboardSection", mappedBy="storyboard", orphanRemoval=true, cascade={"persist", "remove"})
44+
* @ORM\OneToMany(targetEntity="StoryboardSection", mappedBy="storyboard", orphanRemoval=true, cascade={"persist"})
4545
*/
4646
#[ApiProperty(readableLink: true, writableLink: false)]
4747
#[Groups(['read'])]

api/src/Entity/SortableEntityTrait.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22

33
namespace App\Entity;
44

5+
use Symfony\Component\Serializer\Annotation\Groups;
6+
57
trait SortableEntityTrait {
68
/**
79
* @ORM\Column(type="integer", nullable=false)
810
*/
911
private int $pos = 0;
1012

13+
#[Groups(['read'])]
1114
public function getPos(): int {
1215
return $this->pos;
1316
}
1417

18+
#[Groups(['write'])]
1519
public function setPos(int $pos): void {
1620
$this->pos = $pos;
1721
}

0 commit comments

Comments
 (0)