4

I have these models:

  • Organisation

  • Student

  • Course

  • Enrollment

A Student belongs to an Organisation

A Student can enrol on 1 or more courses

So an Enrollment record basically consists of a given Course and a given Student

from django.db import models
from model_utils.models import TimeStampedModel

class Organisation(TimeStampedModel):
  objects = models.Manager()
  name = models.CharField(max_length=50)

  def __str__(self):
    return self.name

class Student(TimeStampedModel):
  objects = models.Manager()
  first_name = models.CharField(max_length=50)
  last_name = models.CharField(max_length=50)
  email = models.EmailField(unique=True)
  organisation = models.ForeignKey(to=Organisation, on_delete=models.SET_NULL, default=None, null=True)

  def __str__(self):
    return self.email

class Course(TimeStampedModel):
  objects = models.Manager()
  language = models.CharField(max_length=30)
  level = models.CharField(max_length=2)

  def __str__(self):
    return self.language + ' ' + self.level

  class Meta:
    unique_together = ("language", "level")

class EnrollmentManager(models.Manager):
  def org_students_enrolled(self, organisation):
    return self.filter(student__organisation__name=organisation).all()

class Enrollment(TimeStampedModel):
  objects = EnrollmentManager()
  course = models.ForeignKey(to=Course, on_delete=models.CASCADE, default=None, null=False, related_name='enrollments')
  student = models.ForeignKey(to=Student, on_delete=models.CASCADE, default=None, null=False, related_name='enrollments')
  enrolled = models.DateTimeField()
  last_booking = models.DateTimeField()
  credits_total = models.SmallIntegerField(default=10)
  credits_balance = models.DecimalField(max_digits=5, decimal_places=2)

Notice the custom EnrollmentManager that allows me to find all students who are enrolled from a given organisation.

How can I add a custom Manager to retrieve all the courses from a given organisation whose students are enrolled?

What I have tried

I thought to create a CourseManager and somehow query/filter from that side of the relationship:

class CourseManager(models.Manager):
  def org_courses_enrolled(self, organisation):
    return self.filter(enrollment__student__organisation__name=organisation).all()

This works, but it gives me the same 100 enrollment records :(

What I am trying to get is: based on a given organisation find all students who are enrolled and then (DISTINCT?) to get the list of enrolled courses for that org

This is the view:

class OrganisationCoursesView(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
  serializer_class = CourseSerializer
  queryset = Course.objects.get_courses(1)

and the url:

# The below should allow: /api/v1/organisations/1/courses/
router.register('api/v1/organisations/(?P<organisation_pk>\d+)/courses', OrganisationCoursesView, 'organisation courses')

UPDATE 1

Based on the answer from h1dd3n I tried this next:

class CourseManager(models.Manager):

  def get_queryset(self):
     return super(CourseManager, self).get_queryset()

  def get_courses(self, organisation):
    return self.get_queryset().filter(student__organisation_id=organisation)

but that throws an error (as I expected it would):

FieldError: Cannot resolve keyword 'student' into field. Choices are: courses, created, id, language, level, modified, progress

UPDATE 2 - getting closer!

Ok with help from @AKX's comments:

class CourseManager(models.Manager):

  def get_queryset(self):
     return super(CourseManager, self).get_queryset()

  def get_courses(self, organisation):
    return self.get_queryset().filter(courses__student__organisation_id=organisation)

now DOES return courses, but it returns a copy for each enrolled student. So now I need to group them so each record only appears one time...

rmcsharry
  • 5,363
  • 6
  • 65
  • 108
  • 1
    As an aside, since `Organisation.name` isn't unique, using `organisation__name` might return unexpected results if there are multiple organizations with the same name. – AKX Apr 18 '20 at 17:18
  • Good spot thanks, I have since changed it to use the id instead – rmcsharry Apr 18 '20 at 17:23
  • Does `filter(enrollment_set__student__organisation_id=organisation)` work, by any chance? – AKX Apr 18 '20 at 17:32
  • No - Cannot resolve keyword 'enrollment_set' :( Note that the foreign key for a Course is defined in Enrollment (as is the Student foreign key) – rmcsharry Apr 18 '20 at 17:40
  • They'd still have reverse relations, á la https://docs.djangoproject.com/en/3.0/topics/db/queries/#following-relationships-backward – AKX Apr 18 '20 at 17:43
  • "You can override the FOO_set name by setting the related_name parameter in the ForeignKey definition" -> I did that locally but forgot to update the question, which I will now do. – rmcsharry Apr 18 '20 at 17:48

1 Answers1

1

First you need to change self.filter to self.get_queryset().filter() or make a seperate method in the manager.

  def get_queryset(self):
     return super(CourseManager, self).get_queryset()

In manager create a function

  def get_courses(self,organisation): 
     return self.get_queryset.filter(student__oraganisation=organisation)

This should return the students and you don't need to call .all() - the filtered qs either way returns you all the objects that it finds.

EDIT

Try this:

class CourseManager(models.Manager):

  def get_queryset(self):
     return super(CourseManager, self).get_queryset()

  def get_courses(self, organisation):
    return self.get_queryset().filter( \
      enrollments__student__organisation_id=organisation).distinct()

UPDATE 2

You can try and play around with from django.db.models import Q https://docs.djangoproject.com/en/3.0/topics/db/queries/

or with annotate based on this answer Get distinct values of Queryset by field where you filter out each student so it would appear once.

rmcsharry
  • 5,363
  • 6
  • 65
  • 108
h1dd3n
  • 292
  • 1
  • 12
  • Thanks for the quick response, but this doesn't quite get there. The "get_courses" returns students. So I have to use the StudentSerializer. I get 100 students (100 enrollments) showing, not just the 8 courses that 100 students have enrolled on. Somewhere/somehow I need to group enrollments by course id, I think – rmcsharry Apr 18 '20 at 17:05
  • 1
    The Edit gets there ;) – rmcsharry Apr 19 '20 at 23:08