Clarified Django Models and Relationships - Sloathking/Foolish-Wizardz GitHub Wiki

Clarified Django Models and Relationships

Author Brett

Related assignments: All

Problem:

Currently, our database relationships are set up so that a Portfolio contains all the references for both a Student and the Projects in that Portfolio.

Which means if we wanted to delete a Portfolio, then not only do all the Projects in that Portfolio get deleted so does the Student.

But we do not want a Student to exist without also having a Portfolio, and our current relationships ensure that does not happen.

So, to get our relationships represented how we would want them to be:

Student -> Portfolio -> Projects

As well as ensuring that a Student always has a Portfolio, there are a few ways to accomplish this:

  • Overloading the save() method for our Student class.
  • Use Django signals to call a method

Model code for new relationships

  • Student
class Student(models.Model):
    #List of choices for major value in database, human readable name
    MAJOR = (
        ('CSCI-BS', 'BS in Computer Science'),
        ('CPEN-BS', 'BS in Computer Engineering'),
        ('BIGD-BI', 'BI in Game Design and Development'),
        ('BICS-BI', 'BI in Computer Science'),
        ('BISC-BI', 'BI in Computer Security'),
        ('CSCI-BA', 'BA in Computer Science'),
        ('DASE-BS', 'BS in Data Analytics and Systems Engineering')
    )
    name = models.CharField(max_length=200)
    email = models.CharField("UCCS Email", max_length=200)
    major = models.CharField(max_length=200, choices=MAJOR)

    def __str__(self): return self.name
    
    def get_absolute_url(self): return reverse('student-detail', args=[str(self.id)])
  • Portfolio
class Portfolio(models.Model):
    title = models.CharField(max_length=200)
    contact_email = models.CharField("Contact Email", max_length=200)
    is_active = False
    about = models.TextField("About", blank=True)
    student = models.OneToOneField(Student, on_delete=models.CASCADE, null=True)

    def __str__(self): return self.title
    
    def get_absolute_url(self): return reverse('portfolio-detail', args=[str(self.id)])
  • Project
class Project(models.Model):
    title = models.CharField(max_length=200)
    description = models.TextField("Project Description")
    portfolio = models.ForeignKey(Portfolio, on_delete=models.CASCADE, null=True)

    def __str__(self): return self.title
    
    def get_absolute_url(self): return reverse('project-detail', args=[str(self.id)])

Overloading the save() method

Overloading the save() method allows us to define custom behavior for whenever objects of that class are saved to the database.

Only issue with this approach is that the save() method is not called on bulk operations.

def save(self, *args, **kwargs):
        is_new = self.id is None
        super(Student, self).save(*args, **kwargs)
        if is_new:
            p = Portfolio.objects.create(student=self)
            p.title = self.name + "'s Portfolio"
            p.contact_email = self.email
            p.save()

This is all this approach needs to do what we need it to do.

Django Signals

This approach makes use of the post-save Signal that Django has built-in, which is a system-wide notification for specific actions that happen. The downside to using this method is that since there are no restrictions on where the Signals code needs to be placed, they can make debugging more difficult.

To make use of Signals they need to be imported:

from django.db.models.signals import post_save
from django.dispatch import receiver

The code to make use of the 'post-save' signal:

@receiver(post_save, sender=Student)
def create_portfolio(sender, instance, created, **kwargs):
    if created:
        p = Portfolio.objects.create(student=instance)
        p.title = instance.name + "'s Portfolio"
        p.contact_email = instance.email
        p.save()

The method used with the Signal doesn't change, but we need a decorator to keep an ear out for our signal.