Django (DRF) ManyToMany field choices / limit

Viewed 488

Working with Django REST Framework I am wondering if it's possible to limit the choices / options of a ManyToMany field on a model to a specific QuerySet?

Using the models below (scroll down to see models), I am curious if it's possible to define this limit in the definition of the model, to achieve the following:

# Having the following Employee instance
emp = Employee(...)

# Should return only the instances with value 'case' in EmployeeSubstitute.type field
emp.substitute_case.all()

# Should return only the instances with value 'phone' in EmployeeSubstitute.type field
emp.substitute_phone.all()

Models:

class Employee(models.Model):
    substitute_case = models.ManyToMany(through=EmployeeSubstitute, ...)
    substitute_phone = models.ManyToMany(through=EmployeeSubstitute, ...)
class EmployeeSubstitute(models.Model):
    from = models.ForeignKey(Employee, ...)
    to = models.ForeignKey(Employee, ...)
    type = models.CharField(choices=..., ...)  # choose between type 'case' and 'phone'

I see that there's the limit_choices_to parameter, but that's not what I am looking for, since that only effects the options shown when using a ModelForm or the admin.

1 Answers

Well, ManyToManyField returns related objects and as docs state

By default, Django uses an instance of the Model._base_manager manager class when accessing related objects (i.e. choice.question), not the _default_manager on the related object. This is because Django needs to be able to retrieve the related object, even if it would otherwise be filtered out (and hence be inaccessible) by the default manager.

If the normal base manager class (django.db.models.Manager) isn’t appropriate for your circumstances, you can tell Django which class to use by setting Meta.base_manager_name.

Base managers aren’t used when querying on related models, or when accessing a one-to-many or many-to-many relationship. For example, if the Question model from the tutorial had a deleted field and a base manager that filters out instances with deleted=True, a queryset like Choice.objects.filter(question__name__startswith='What') would include choices related to deleted questions.

So if I read it correctly, no, it's not possible.

When you do queries and have through in your ManyToManyField, Django complains you should run these queries on your through model, rather than the "parent". I can't find it in the docs but I remember seeing it a few times.

substitute_case and substitute_phone is something that belongs to substitute and it is it's type. So just do that instead of creating those columns in Employee.

from django.db import models


class SubstituteTypes(models.TextChoices):
    case = "case", "case"
    phone = "phone", "phone"


class EmployeeSubstituteQueryset(models.QuerySet):
    def from_employee(self, e):
        return self.filter(_from=e)

    def case(self):
        return self.filter(type=SubstituteTypes.case)

    def phone(self):
        return self.filter(type=SubstituteTypes.phone)


class Employee(models.Model):
    substitute = models.ManyToManyField(through='EmployeeSubstitute', to='self')


class EmployeeSubstitute(models.Model):
    _from = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='a')
    to = models.ForeignKey(Employee, on_delete=models.PROTECT, related_name='b')
    type = models.CharField(choices=SubstituteTypes.choices, max_length=5, db_index=True)

    objects = EmployeeSubstituteQueryset.as_manager()

Then, once you get your emp object (or only its id), you can do

EmployeeSubstitute.objects.from_employee(emp).case().all()

which is designed in Django philosophy.

Related