ecamp3
ecamp3 copied to clipboard
Proposal for ContentNode refactor
- Avoid class/table inheritance
- One ContentNode class, structured as proper tree (as-is now) or as nested set (feasability to be evaluated)
contentTypeproperty defines the type (as-is today)dataproperty of typejsonbholds the actual content- Some validation possible via schema validation (similar as already implemented for ColumnLayout->columns)
Limitations/Notes
- Validation might be possible to a limited extent only (schema validation) - more responsibility to the frontend.
- CleanHTML probably still required --> needs manual validation inside DataPersister
- Concurrency when editing might be an issue, when data is completely replaced with each write (could be mitigated by implementing partial updates - either by merging the json or by using Postgres specific json functions)
Variation
- Sub data like
Storyboard->sectionscould be separate content nodes instead of integrated into thedatajson property (this could mitigate concurrency issues) - In this case, some control mechanism is needed to define allowed tree structures (e.g. StoryboardSection can be a child of Storyboard only)
Other notes
- https://github.com/dunglas/doctrine-json-odm might be interesting to look into, but probably not needed
- Evaluate implementation as nested set https://github.com/blt04/doctrine2-nestedset
Doctrine entity
class ContentNode extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface {
/**
* The camp to which this contet node belongs. May not be changed once the content node is created.
*/
#[ORM\ManyToOne(targetEntity: Camp::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'cascade')]
public ?Camp $camp = null;
/**
* The content node that is the root of the content node tree. Refers to itself in case this
* content node is the root.
*/
#[ORM\ManyToOne(targetEntity: ContentNode::class, inversedBy: 'rootDescendants')]
#[ORM\JoinColumn(nullable: true)]
public ContentNode $root;
/**
* All content nodes that are part of this content node tree.
*/
#[ApiProperty(readable: false, writable: false)]
#[ORM\OneToMany(targetEntity: ContentNode::class, mappedBy: 'root')]
public Collection $rootDescendants;
/**
* 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. A content node is defined as root when
* it has an owner.
*/
#[ORM\ManyToOne(targetEntity: ContentNode::class, inversedBy: 'children')]
#[ORM\JoinColumn(onDelete: 'CASCADE')]
public ?ContentNode $parent = null;
/**
* All content nodes that are direct children of this content node.
*/
#[ORM\OneToMany(targetEntity: ContentNode::class, mappedBy: 'parent', cascade: ['persist'])]
public Collection $children;
/**
* Holds the actual data of the content node
*/
#[ORM\Column(type: 'json', nullable: true, options: ["jsonb" => true])]
public ?array $data = null;
/**
* 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.
*/
#[ORM\Column(type: 'text', nullable: true)]
public ?string $slot = null;
/**
* 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.
*/
#[ORM\Column(type: 'integer', nullable: false)]
public int $position = -1;
/**
* 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.
*/
#[ORM\Column(type: 'text', nullable: true)]
public ?string $instanceName = null;
/**
* 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.
*/
#[ORM\ManyToOne(targetEntity: ContentType::class)]
#[ORM\JoinColumn(nullable: false)]
public ?ContentType $contentType = null;
}
Example API response (without variation)
{
"_links": {
"self": {
"href": "/content_nodes/8e2119b66e8f"
},
"root": {
"href": "/content_nodes/d81a9dae4316"
},
"parent": {
"href": "/content_nodes/945b99f32295"
},
"children": [],
"contentType": {
"href": "/content_types/cfccaecd4bad"
},
"camp": {
"href": "/camps/c4cca3a51342"
}
},
"slot": "1",
"position": 0,
"instanceName": null,
"id": "8e2119b66e8f",
"contentTypeName": "Storyboard",
"data": {
"sections": [
{
"column1": null,
"column2": "<p>Wir Treffen uns vor der Küche und alle Kinder werfen ihren Anonymen Brief in denn Pfaditopf. Danach wird nachmals der ganze Ablauf erklärt. </p><p>1. Still zum Bi-Pi Feuer laufen.</p><p>2. Feuer anzünden</p><p>3. Brief</p><p>4. Abteilungslied singen</p><p>5. Private Zettel vorlessen</p>",
"column3": null,
"position": 0
},
{
"column1": null,
"column2": "<p>Zusammen Laufen wir zum Bi-Pi Feuer und geniessen unser Ritual.</p><p>1. Der Feuermeister macht Feuer</p><p>2. Zera liest den Letzten Brief von Bi-Pi vor</p><p>3. Wir Singen das Abteilungslied</p><p>4. Wir geben denn Kessel durch und jeder zieht einen Zettel der er vorliest</p>",
"column3": null,
"position": 1
},
{
"column1": null,
"column2": "<p>laufen die Kinder (vereinzelt) zurück zum Lagerplatz wenn sie alles überdenkt haben.</p><p> Danach gibt es das Lagerfeuer-feeling in der Küche mit Schoggi-Bananen und Marshmallows.</p>",
"column3": null,
"position": 2
},
]
}
}
Example API response (with variation)
{
"_links": {
"self": {
"href": "/content_nodes/8e2119b66e8f"
},
"root": {
"href": "/content_nodes/d81a9dae4316"
},
"parent": {
"href": "/content_nodes/945b99f32295"
},
"children": [
"/content_nodes/f7310f31c767",
"/content_nodes/6f1975b376c1",
"/content_nodes/ed357d92cc86"
],
"contentType": {
"href": "/content_types/cfccaecd4bad"
},
"camp": {
"href": "/camps/c4cca3a51342"
}
},
"slot": "1",
"position": 0,
"instanceName": null,
"id": "8e2119b66e8f",
"contentTypeName": "Storyboard",
"data": null
}
{
"_links": {
"self": {
"href": "/content_nodes/f7310f31c767"
},
"root": {
"href": "/content_nodes/d81a9dae4316"
},
"parent": {
"href": "/content_nodes/8e2119b66e8f"
},
"children": [],
"contentType": {
"href": "/content_types/e8c03e4285cb"
},
"camp": {
"href": "/camps/c4cca3a51342"
}
},
"slot": null,
"position": 0,
"instanceName": null,
"id": "f7310f31c767",
"contentTypeName": "StoryboardSection",
"data": {
"column1": null,
"column2": "<p>Wir Treffen uns vor der Küche und alle Kinder werfen ihren Anonymen Brief in denn Pfaditopf. Danach wird nachmals der ganze Ablauf erklärt. </p><p>1. Still zum Bi-Pi Feuer laufen.</p><p>2. Feuer anzünden</p><p>3. Brief</p><p>4. Abteilungslied singen</p><p>5. Private Zettel vorlessen</p>",
"column3": null
}
}
API response today (as reference)
{
"_links": {
"self": {
"href": "/content_node/storyboards/8e2119b66e8f"
},
"sections": {
"href": "/content_node/storyboard_sections?storyboard=/content_node/storyboards/8e2119b66e8f"
},
"root": {
"href": "/content_node/column_layouts/d81a9dae4316"
},
"parent": {
"href": "/content_node/column_layouts/945b99f32295"
},
"children": [],
"contentType": {
"href": "/content_types/cfccaecd4bad"
},
"owner": {
"href": "/activities/b790b08a6b47"
},
"ownerCategory": {
"href": "/categories/505e3fdf9e90"
}
},
"_embedded": {
"sections": [
{
"_links": {
"self": {
"href": "/content_node/storyboard_sections/ed357d92cc86"
},
"storyboard": {
"href": "/content_node/storyboards/8e2119b66e8f"
}
},
"column1": null,
"column2": "<p>laufen die Kinder (vereinzelt) zurück zum Lagerplatz wenn sie alles überdenkt haben.</p><p> Danach gibt es das Lagerfeuer-feeling in der Küche mit Schoggi-Bananen und Marshmallows.</p>",
"column3": null,
"id": "ed357d92cc86",
"position": 2
},
{
"_links": {
"self": {
"href": "/content_node/storyboard_sections/f7310f31c767"
},
"storyboard": {
"href": "/content_node/storyboards/8e2119b66e8f"
}
},
"column1": null,
"column2": "<p>Wir Treffen uns vor der Küche und alle Kinder werfen ihren Anonymen Brief in denn Pfaditopf. Danach wird nachmals der ganze Ablauf erklärt. </p><p>1. Still zum Bi-Pi Feuer laufen.</p><p>2. Feuer anzünden</p><p>3. Brief</p><p>4. Abteilungslied singen</p><p>5. Private Zettel vorlessen</p>",
"column3": null,
"id": "f7310f31c767",
"position": 0
},
{
"_links": {
"self": {
"href": "/content_node/storyboard_sections/6f1975b376c1"
},
"storyboard": {
"href": "/content_node/storyboards/8e2119b66e8f"
}
},
"column1": null,
"column2": "<p>Zusammen Laufen wir zum Bi-Pi Feuer und geniessen unser Ritual.</p><p>1. Der Feuermeister macht Feuer</p><p>2. Zera liest den Letzten Brief von Bi-Pi vor</p><p>3. Wir Singen das Abteilungslied</p><p>4. Wir geben denn Kessel durch und jeder zieht einen Zettel der er vorliest</p>",
"column3": null,
"id": "6f1975b376c1",
"position": 1
}
]
},
"slot": "1",
"position": 0,
"instanceName": null,
"id": "8e2119b66e8f",
"contentTypeName": "Storyboard"
}
So for you https://github.com/usu/ecamp3/pull/7 is not an option?
So for you usu#7 is not an option?
usu#7 oder ähnlich wäre für mich das Minimum. Der Vorschlag hier geht halt ein gutes Stück weiter mit verschiedenen Pros/Cons. Wahrscheinlich ersparen wir uns halt einige Headaches, wenn wir versuchen die Inheritance zu umgehen. Auf der anderen Seite halt schon ein wenig ein Konzeptwechsel.
Current state of affairs: https://github.com/usu/ecamp3/pull/8 (still very experimental)
Implemented in #2825