6

I am a new user of Django, and I am trying to figure out how to created a model which can support many kind (type) of elements.

This is the plot : I want to create a Blog module on my application. To do this, I created a model Page, which describe a Blog Page. And a model PageElement, which describe a Post on the blog. Each Page can contain many PageElement.

A PageElement can have many types, because I want my users could post like just a short text, or just a video, or just a picture. I also would like (for example) the user could just post a reference to another model (like a reference to an user). Depending of the kind of content the user posted, the HTML page will display each PageElement in a different way.

But I don't know what is the right way to declare the PageElement class in order to support all these cases :(

Here is my Page model :

class Page(models.Model):
    uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

    # Basical informations
    title = models.CharField(max_length=150)
    description = models.TextField(blank=True)

    # Foreign links
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        related_name='pages_as_user'
    )

    created_at = models.DateTimeField(default=timezone.now)

    # Other fields ....

    class Meta:
        indexes = [
            models.Index(fields=['uuid']),
            models.Index(fields=['user', 'artist'])
        ]

For now, I have two solutions, the first one use inheritance : When you create a new post on the blog, you create an Element which inherit from PageElement model. Here are my different Models for each cases :

class PageElement(models.Model):
    page = models.ForeignKey(
        Page,
        on_delete=models.CASCADE,
        related_name='%(class)s_elements'
    )

    updated_at = models.DateTimeField(default=timezone.now)
    created_at = models.DateTimeField(default=timezone.now)

class PageImageElement(PageElement):
    image = models.ImageField(null=True)
    image_url = models.URLField(null=True)

class PageVideoElement(PageElement):
    video = models.FileField(null=True)
    video_url = models.URLField(null=True)

class PageTextElement(PageElement):
    text = models.TextField(null=True)

class PageUserElement(PageElement):
    user = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
        related_name='elements'
    )

This solution would be the one I have choosen if I had to work with "pure" Python. Because I could stored each PageElement in a dictionnary and filter them by class. And this solution could be easily extended in the futur with new type of content.

But with Django models. It seems that is not the best solution. Because it will be really difficult to get all PageElement children from the database (I can't just write "page.elements" to get all elements of all types, I need to get all %(class)s_elements elements manually and concatenate them :/). I have thinked about a solution like below (I don't have tried it yet), but it seems overkilled for this problem (and for the database which will have to deal with a large number of request):

class Page(models.Model):
    # ...
    def get_elements(self):
        # Retrieve all PageElements children linked to the current Page
        R = []
        fields = self._meta.get_fields(include_hidden=True)
        for f in fields:
            try:
                if '_elements' in f.name:
                    R += getattr(self, f.name)
            except TypeError as e:
                continue

        return R

My second "solution" use an unique class which contains all fields I need. Depending of the kind of PageElement I want to create, I would put type field to the correct value, put the values in the corresponding fields, and put to NULL all other unused fields :

class PageElement(models.Model):
    page = models.OneToOneField(
        Page,
        on_delete=models.CASCADE,
        related_name='elements'
    )

    updated_at = models.DateTimeField(default=timezone.now)
    created_at = models.DateTimeField(default=timezone.now)

    TYPES_CHOICE = (
        ('img', 'Image'),
        ('vid', 'Video'),
        ('txt', 'Text'),
        ('usr', 'User'),
    )
    type = models.CharField(max_length=60, choices=TYPES_CHOICE)

    # For type Image
    image = models.ImageField(null=True)
    image_url = models.URLField(null=True)

    # For type Video
    video = models.FileField(null=True)
    video_url = models.URLField(null=True)

    # For type Text
    text = models.TextField(null=True)

    # For type User
    user = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
        related_name='elements',
        null=True
    )

With this solution, I can retrieve all elements in a single request with "page.elements". But it is less extendable than the previous one (I need to modify my entire table structure to add a new field or a new kind of Element).

To be honnest, I have absolutly no idea of which solution is the best. And I am sure other (better) solutions exist, but my poor Oriented-Object skills don't give me the ability to think about them ( :( )...

I want a solution which can be easily modified in the future (if for example, I want to add a new Type "calendar" on the Blog, which reference a DateTime). And which would be easy to use in my application if I want to retrieve all Elements related to a Page...

Thanks for your attention :)

graille
  • 1,131
  • 2
  • 14
  • 33
  • You can create model like `text`, `image` `video` etc. Now create your page model, add text with a foreign key to model text, image with model image and so on. In case you want to add gif you can add that to your image model, and your page model don't need any change. – Bidhan Majhi Oct 29 '18 at 09:37
  • 1
    I don't really understand your solution :/ Do you have an example ? – graille Oct 30 '18 at 06:32
  • I think you can consider solutions for dynamic fields problem https://stackoverflow.com/questions/7933596/django-dynamic-model-fields – Yohanes Gultom Nov 04 '18 at 14:19
  • `__Each Page can contain many PageElement__`, It's not satisfying according to your model, But really I'm unable to understand your problem. could you please write a problem statement. – Mr Singh Nov 05 '18 at 09:17
  • First point: it's not as much of an OO issue as a relational model (SQL) one - Django models are really just thin wrapper over a relational database and you have to deal with it. Second point: this is not really a new problem, most CMS have to solve this, so you may want to check how they did (you can start here : https://djangopackages.org/grids/g/cms/ - at least django-cms, feincms and django-fluent-pages have a similar "a page is composed of elements" design). – bruno desthuilliers Nov 05 '18 at 10:08

3 Answers3

2

I'm not sure it fits your problem but using GenericForeignKeys/ContentType framework may be appropriate in this case. It's quite powerful when one grasps the concept.

Example construct:

class Page(models.Model):
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    page_element = GenericForeignKey('content_type', 'object_id')
    ...

You can now connect any model object by the GenericFK to the Page model. So adding a new type (as a new model), at a later stage, is not intrusive.

Update:

As a comment pointed out this construct doesn't support many PageElements in a good way for a Page.

To elaborate, one way to solve that problem, still taking advantage of the GenericFK...

class PageElement(models.Model):
    class Meta:
        unique_together=('page', 'content_type', 'object_id') # Solve the unique per page

     page = models.ForeignKey(Page, related_name='page_elements')
     content_type = models.ForeignKey(ContentType)
     object_id = models.PositiveIntegerField()
     content_object = GenericForeignKey('content_type', 'object_id')

A Page can have many "abstract" PageElements and content_object is the "concrete PageElement model/implementation". Easy to retrieve all elements for a specific page and allows inspection of the ContentType to check the type of element etc.

Just one way of many to solve this particular problem.

Daniel Backman
  • 5,121
  • 1
  • 32
  • 37
  • 1
    This is the right way to do this. Specifically [generic relations](https://docs.djangoproject.com/en/2.1/ref/contrib/contenttypes/#generic-relations) are what you want. – Ananth Nov 05 '18 at 10:21
  • I think that Elements should ForeignKey to Page, not the other way round, as we want to define that every page contains multiple modules, which are unique to that particular page. In the solution you proposed several pages would possibly fk to the same PageVideoElement or PageImageElement module. So different posts, would possibly contain the same vid or image, definitely not the result we are going after. – Daniel Kislyuk Nov 05 '18 at 10:51
  • I will have a look on it when I will be back in my office. But your solution seems really good :). Thanks – graille Nov 11 '18 at 11:47
0

To establish the relationship between Page and PageElement in Django you would rather use Foreign Key relationship, than inheritance.

class PageImageElement(PageElement):
    page = models.ForeignKey(Page,
                             on_delete=models.CASCADE,
                             related_name='images')
    image = models.ImageField(null=True)
    image_url = models.URLField(null=True)

Every user's post would create an instance of Page. Every addition of image to the Page would create an instance of PageImageElement and you could query for them using the related name. This way would be really easy to access all video, image, text modules of a single Page.

On a related note, I would say that PageElement class could be abstract see the docs and if you declare fields as possibly containing null values as in video = models.FileField(null=True) then it might be worth declaring blank=True as well, otherwise there will be errors when creating the object with these fields undefined. Discussed, for example, here: differentiate null=True, blank=True in django

Daniel Kislyuk
  • 956
  • 10
  • 11
0

I can't just write "page.elements" to get all elements of all types

Well actually, you can if you use multi-table inheritance. The problem is that all records returned are instances of PageElement, meaning you lose all information of the subclass type and the additional data these child objects may hold.
There are quite a lot of packages that tackle this polymorphism problem: django packages: Model inheritance

CoffeeBasedLifeform
  • 2,296
  • 12
  • 27