Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for ContentNode refactor #2633

Closed
usu opened this issue Apr 20, 2022 · 4 comments
Closed

Proposal for ContentNode refactor #2633

usu opened this issue Apr 20, 2022 · 4 comments

Comments

@usu
Copy link
Member

usu commented Apr 20, 2022

  • Avoid class/table inheritance
  • One ContentNode class, structured as proper tree (as-is now) or as nested set (feasability to be evaluated)
  • contentType property defines the type (as-is today)
  • data property of type jsonb holds 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->sections could be separate content nodes instead of integrated into the data json 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

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"
}
@usu usu added the Meeting Discuss Am nächsten Core-Meeting besprechen label Apr 20, 2022
@BacLuc
Copy link
Contributor

BacLuc commented Apr 24, 2022

So for you usu#7 is not an option?

@usu
Copy link
Member Author

usu commented Apr 24, 2022

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.

@usu
Copy link
Member Author

usu commented May 11, 2022

Current state of affairs: usu#8
(still very experimental)

@usu usu removed the Meeting Discuss Am nächsten Core-Meeting besprechen label Aug 25, 2022
@usu
Copy link
Member Author

usu commented Oct 1, 2022

Implemented in #2825

@usu usu closed this as completed Oct 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants