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.OneToOneField( 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 if a “pure” Django. 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 'conversation_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 🙂