MorganFreeFarm
MorganFreeFarm

Reputation: 3733

Proper implementation of Django model inheritance architecture?

I'm building a simple Django app for reviews of different objects (restaurants, car services, car wash etc).

I started with the app, but soon I faced first problem. Every object has features (but every type of object has different features).

For example:

So I started to build a typical DB implementation with ManyToMany tables, but then I found Django Model Inheritance, so I implement it in my APP, as you can see:

urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('user/<int:pk>/', views.UserObjectsView.as_view(), name='user-objects'),
    path('add/', views.add_object, name='add-object'),
    path('<str:category>/<int:pk>/', views.show_object, name='show-object'),
    path('all/<str:category>/', views.show_all_objects, name="show-all-objects"),
]

models.py:

from django.db import models
from users.models import ProfileUser
from django.utils import timezone

# Create your models here.

class Object(models.Model):
    author = models.ForeignKey(ProfileUser, on_delete=models.CASCADE)
    title = models.CharField(max_length=300)
    address = models.CharField(max_length=300)
    content = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    approved_object = models.BooleanField(default=False)
    admin_seen = models.BooleanField(default=False)

    def __str__(self):
        return f"{self.title}"


class Restaurant(Object):
    seats = models.IntegerField()
    bulgarian_kitchen = models.BooleanField(default=False)
    italian_kitchen = models.BooleanField(default=False)
    french_kitchen = models.BooleanField(default=False)
    is_garden = models.BooleanField(default=False)
    is_playground = models.BooleanField(default=False)


class SportFitness(Object):
    is_fitness_trainer = models.BooleanField(default=False)


class CarService(Object):
    is_parts_clients = models.BooleanField(default=False)


class BeautySalon(Object):
    is_hair_salon = models.BooleanField(default=False)
    is_laser_epilation = models.BooleanField(default=False)


class FastFood(Object):
    is_pizza = models.BooleanField(default=False)
    is_duner = models.BooleanField(default=False)
    is_seats = models.BooleanField(default=False)


class CarWash(Object):
    is_external_cleaning = models.BooleanField(default=False)
    is_internal_cleaning = models.BooleanField(default=False)
    is_engine_cleaning = models.BooleanField(default=False)


class Fun(Object):
    is_working_weekend = models.BooleanField(default=False)
    is_kids_suitable = models.BooleanField(default=False)


class Other(Object):
    is_working_weekend = models.BooleanField(default=False)


class Comment(models.Model):
    object = models.ForeignKey(Object, on_delete=models.CASCADE, related_name='comments')
    author = models.ForeignKey(ProfileUser, on_delete=models.CASCADE)
    content = models.TextField()
    rating = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return f"{self.content}"

views.py:

from django.shortcuts import render, redirect
from django.views import generic
from objects.models import Object, ProfileUser, Comment, Restaurant, SportFitness, CarService, BeautySalon, FastFood, CarWash, Fun, Other
from .forms import ObjectForm, CommentForm
from django.contrib import messages
from django.db.models import Avg
import sys, pdb

class AllObjects(generic.ListView):
    queryset = Object.objects.all()
    template_name = 'show_all_objects.html'

class UserObjectsView(generic.ListView):
    template_name = 'user_objects.html'

    def get_queryset(self):
        user_id = self.kwargs['pk']
        return Object.objects.filter(author_id = user_id)

def add_object(request):
    if not request.user.is_authenticated:
        messages.info(request, 'За да добавите нов Обект, трябва да сте регистриран потребител!')
        return redirect('account_login')
    form = ObjectForm(request.POST or None)
    if form.is_valid():
        obj = form.save(commit=False)
        obj.author = ProfileUser.objects.get(user=request.user)
        obj.save()
        messages.success(request, 'Успешно добавихте нов Обект, може да видите вашите обекти във вашия профил!')
        return redirect('home')

    context = {
        'form': form
    }

    return render(request, "add_object.html", context)

def show_object(request, pk):
    obj = Object.objects.get(id=5)

    if request.method == 'POST':
        user = request.user
        author = ProfileUser.objects.get(user=user)
        comment = Comment()
        comment.object = obj
        comment.author = author
        comment.content = request.POST.get('content')
        comment.rating = request.POST.get('rating')
        comment.save()

    form = CommentForm()
    reviews_count = Comment.objects.filter(object_id=pk).count()
    rating = Comment.objects.filter(object_id=pk).aggregate(Avg('rating'))['rating__avg']

    context = {
        'form': form,
        'object': obj,
        'reviews_count': reviews_count,
        'rating': rating
    }

    return render(request, "show_object.html", context)

def show_all_objects(request, category):
    categories = {'restaurants' : 'Restaurant', 'sportfitness' : 'SportFitness', 'carservice' : 'CarService', 'beautysalon' : 'BeautySalon', 'fastfood' : 'FastFood', 'carwash' : 'CarWash', 'fun' : 'Fun', 'other' : 'Other'}
    objects = eval(categories[category]).objects.all()
    context = {
        'object_list': objects,
    }

    return render(request, 'show_all_objects.html', context)

Everything was fine until I had to show objects for each category, like this (I'm using restaurants instead of Restaurant, because url looks much better):

  <a href="{% url 'show-all-objects' category='restaurants' %}" class="utf_category_small_box_part"> <i class="im im-icon-Chef"></i>
    <h4>Ресторантии</h4>
    <span>22</span>
  </a>
  <a href="{% url 'show-all-objects' category='sportfitness' %}" class="utf_category_small_box_part"> <i class="im im-icon-Dumbbell"></i>
    <h4>Спортни и фитнес</h4>
    <span>15</span>
  </a>

You can check show_all_objects function, so I did it with eval():

categories = {'restaurants' : 'Restaurant', 'sportfitness' : 'SportFitness', 'carservice' : 'CarService', 'beautysalon' : 'BeautySalon', 'fastfood' : 'FastFood', 'carwash' : 'CarWash', 'fun' : 'Fun', 'other' : 'Other'}
objects = eval(categories[category]).objects.all()

It was fine, but then I faced this problem again. When I want to show an object with its features in show_object()-method (you can check it in code above), I can get the object, but I cannot get restaurant for example, or car wash etc:

def show_object(request, pk):
    obj = Object.objects.get(id=5)

Now I have the object, but I cannot get the exact object. I get the same problem again when I want to show a different form for each type of object (for example restaurants should have a form with checkboxes with its features, carwashes form with checkboxes for its features etc).

P.S: Based on @Saawhat answer, he recommends me to use eval() in show_object() function, but I foreach all ojbects in template, so I cannot pass param like category to it:

<div class="row">
    {% for object in object_list %}
      <div class="col-lg-12 col-md-12">
        <div class="utf_listing_item-container list-layout"> <a href="{% url 'show-object' category='' pk=object.id%}"  class="utf_listing_item">
          <div class="utf_listing_item-image">
              <img src="{% static 'core/images/utf_listing_item-01.jpg' %}" alt="">
              <span class="like-icon"></span>
              <span class="tag"><i class="im im-icon-Hotel"></i> Hotels</span>
              <div class="utf_listing_prige_block utf_half_list">
                <span class="utf_meta_listing_price"><i class="fa fa-tag"></i> $25 - $45</span>
                <span class="utp_approve_item"><i class="utf_approve_listing"></i></span>
              </div>
          </div>
          <div class="utf_listing_item_content">
            <div class="utf_listing_item-inner">
              <h3>{{ object.title }}</h3>
              <span><i class="sl sl-icon-location"></i> {{ object.address }}</span>
              <p>{{ object.content }}</p>
            </div>
          </div>
          </a>
        </div>
      </div>
    {% endfor %}
</div>

This row:

<a href="{% url 'show-object' category='' pk=object.id%}"

I cannot pass category, because I don't have one

Upvotes: 3

Views: 587

Answers (2)

rabbit.aaron
rabbit.aaron

Reputation: 2607

Consider saving the object type to the database, in your object model, add a foreign key to ContentType.

from django.contrib.contenttypes.models import ContentType

class Object(models.Model):

    # optional, but this design would also work for abstract inheritance
    class Meta:
        abstract = True

    # ...
    content_type = models.ForeignKey(ContentType)

    def save(*args, **kwargs):
        self.content_type = ContentType.objects.get_for_model(self.__class__)
        super().save(*args, **kwargs)

Then in your template:

<a href="{% url 'show-object' category=object.content_type.model pk=object.id %}">

You can then find your model in your views like so:

def show_object(request, category, pk):
    # swap out `myapp` to your app name
    # if you don't care about url being friendly
    # you can use content type primary key instead of model name
    model = ContentType.objects.get(app_label="myapp", model=category).model_class()
    obj = model.objects.get(pk=pk)
    # ...

def show_all_objects(request, category):
    model = ContentType.objects.get(app_label="myapp", model=category).model_class()
    all_objects = model.objects.all()
    # ...

Upvotes: 1

Sawant Sharma
Sawant Sharma

Reputation: 758

1: When using inheritance for models put

class Meta:
    abstract = True

in your base model, which is Object in your case so that it doesn't create its own instance/table. So in this case your model will look like

class Object(models.Model):
    author = models.ForeignKey(ProfileUser, on_delete=models.CASCADE)
    title = models.CharField(max_length=300)
    address = models.CharField(max_length=300)
    content = models.TextField()
    created_date = models.DateTimeField(default=timezone.now)
    approved_object = models.BooleanField(default=False)
    admin_seen = models.BooleanField(default=False)

    class Meta:
        abstract = True

    def __str__(self):
        return f"{self.title}"

On other note rename Object to something else like BaseModel or something.

2: When you're calling doing show_object pass the category/name of the model & query on that, eg:

obj = Object.objects.get(id=5) #Instead of this
obj = eval(categories[category]).objects.get(pk=pk) # Do this

3: For your Comment model, either you add bunch of foreign keys for each of your model or you can use Generic Relation in Django.

4: While showing forms use respective models not Object model. You anyways have access to all the fields because you're inheriting from it.

I hope I've answered all of your questions, if not let me know.

Update: Regarding you not being able to pass category from template, in your views pass category also to the template

def show_all_objects(request, category):
    categories = {'restaurants' : 'Restaurant', 'sportfitness' : 'SportFitness', 'carservice' : 'CarService', 'beautysalon' : 'BeautySalon', 'fastfood' : 'FastFood', 'carwash' : 'CarWash', 'fun' : 'Fun', 'other' : 'Other'}
    objects = eval(categories[category]).objects.all()
    context = {
      'object_list': objects,
      'category': category
    }

    return render(request, 'show_all_objects.html', context)

Now you can access category in your template file & pass it back.

Upvotes: 5

Related Questions