Writing "generic" methods which relate to foreign key fields and models for abstract base classes

Question:
Is it possible to generically define methods within an abstract base class which relate to foreign key fields (which I cannot define yet on the abstract base class), and to the respective models from these foreign key fields?

I would need that to work with both the model’s clean() method as well as for property methods.

Is it maybe possible to create a sort of placeholder field within the method’s definition, that gets replaced with a specific field and model for each actual implementation of that base class?

For example, can I def my_method(self, foreign_key_field_1, model_1, foreign_key_field_2, model_2): within the abstract base class, with all not-yet-defined foreign key fields and respective models as parameters? And then within the actual implementation of that base class, I’d put something like:

def my_method(self):
    super().my_method(
        self.foreign_key_field_1,
        ModelToWhichFK1Relates,
        self.foreign_key_field_2,
        ModelToWhichFK2Relates
    )

The approach seems logical to me but I also seem to be missing at least one more step.

Code example:
This is a simplified version of what I’m looking to achieve (with the method definition not yet moved to the abstract base classes, though) for illustration purposes.

Abstract base classes:

class TaskBaseClass(models.Model):
    class TaskStatus(models.TextChoices):
        TODO = "td", "ToDo"
        FINISHED = "fi", _("Finished")
        WORK_IN_PROGRESS = "wp", _("Work in progress")
        PAUSED = "ps", _("Paused")
        CANCELLED = "ca", _("Cancelled")

    id = models.UUIDField(
        primary_key=True,
        default=uuid.uuid4,
        editable=False)

    title = models.CharField(
        max_length=255,
        verbose_name=_("Title"),
    )

    description = models.TextField(
        verbose_name=_("Description"),
    )

    task_status = models.CharField(
        max_length=2,
        choices=TaskStatus.choices,
        default=TaskStatus.TODO,
    )

    class Meta:
        abstract = True


class TaskTemplate(TaskBaseClass):
    """
    A task template defines many (but not necessarily all) fields of a task for the efficient generation of routine tasks.
    Most fields are inherited from TaskBaseClass. Some fields, however, are different between a Task and a TaskTemplate.
    """

    class IntervalDefinition(models.TextChoices):
        NO_INTERVAL = "no", _("No interval")
        DURATION_AFTER_LAST_COMPLETION = "du", _(
            "Interval starts after last task completion"
        )
        FIXED_INTERVAL = (
            "fi",
            _("Interval is based on an absolute schedule (e.g. each January)"),
        )

    task_status = None

    interval_definition = models.CharField(
        max_length=2,
        choices=IntervalDefinition.choices,
        verbose_name=_("Interval definition"),
    )

    due_date_interval_days = models.IntegerField(
        null=True,
        blank=True,
        verbose_name=_("Due date interval (days)"),
    )

    due_date_interval_months = models.IntegerField(
        null=True,
        blank=True,
        verbose_name=_("Due date interval (months)"),
    )

    date_based_recurrence = RecurrenceField(
        null=True, blank=True, verbose_name=_("Date-based recurrence")
    )

    class Meta:
        abstract = True

Model implementations - estimated_due_date_by_meter_reading() and clean() are currently defined at this level but I’d rather move these definitions to the abstract base class instead:

class MaintenanceTask(Task):
    maintenance_task_template = models.ForeignKey(
        MaintenanceTaskTemplate,
        null=True,
        on_delete=models.SET_NULL,
        blank=True,
        verbose_name=_("Maintenance plan"),
    )
    equipment = models.ForeignKey(
        Equipment,
        on_delete=models.RESTRICT,
        verbose_name=_("Equipment"),
    )

   # ...

   @property
    def estimated_due_date_by_meter_reading(self):
        estimated_due_date = None
        # ...
        average_meter_reading_increase_per_day = (
            Equipment.objects.filter(pk=self.equipment.pk)
            .values_list("meter_reading_daily_increase", flat=True)
            .first()
        )

        estimated_due_date = ... # abbreviated
        return estimated_due_date

    def clean(self):
        # ...
        if self.maintenance_task_template is not None:
            template_object = MaintenanceTaskTemplate.objects.get(
                pk=self.maintenance_task_template.pk
            )
        # ... (various if- and match/case-statements to create a new task based on the associated template)

I guess I would have to pass both, specific model fields such as self.equipment or self.maintenance_task_template, as well as the actual models Equipment or MaintenanceTaskTemplate as arguments to the generic method implementations, because the abstract base class doesn’t know about either.

Some context, if helpful:
I’m working on a little project which could be described as a task database for maintenance/repair work on machines in its current stage. It can handle both one-off tasks as well as repeating tasks (e.g. maintenance every 500 operating hours, safety check every 12 months) based on task templates.

I would like to provide the same functionality for different use cases, for example manage repeating tasks for employees (e.g. renew first aid-training every two years, general safety training once a year, …).

The functionality, with regards to the task module, is pretty much the same. (Except for operating hours not being a suitable interval definition for humans. :slight_smile:) But I want to keep the different use cases separate, in order to keep everything clear and also to make access control easier.

I’ve been pondering how to solve this problem for a while, actually wrote but never posted a detailed post on my initial approach incl. diagrams to ask for feedback - and then I remembered that I had read about that topic before on Luke Plant’s blog. My project is even similar to his example - he writes about a task database with different types of owner, I’m working on a task database with different types of “objects” to be worked on.

My initial design fitted Luke’s alternative 2: An intermediate table Object which contains a column per potential kind of “object”, i.e. one FK links to Employees and one FK links to Equipment. I’d have some refactoring to do, introduce some more joins, but it should be doable.

But as I read the blog post again, I noticed alternative 5 as a potentially better fit: Basically, instead of one large Task table for all use cases, I could keep one separate Task table per use case. They would all inherit from one abstract base class. My #1 reason for initially wanting to go a different path seems to be invalid - no need to duplicate all the code when you can simply pass the appropriate class/model as an argument to your functions. (I’m just not sure how that works within the abstract base class and with regards to both classes as models as well as classes as fields…)

That would keep my data structure nice and tidy. I’d highly appreciate that, especially because both the Equipment as well as the Employee model will in future be needed for further functionalities beyond task management, such as keeping track of spare parts associated to a machine.

But for realizing those repeating tasks, I rely on a model clean() method which will, every time a task is finished, checks if that task is associated to a TaskTemplate and if so create a new task according to that template. Just like each use case gets its own Task model in alternative 5, each use case would get its own TaskTemplate model which is not known to TaskBaseClass. But I do not want to duplicate that generic functionality within each use case’s specific Task’s clean() method. Furthermore, I have some property methods with similar challenges.

Thanks in advance for your help!

I’m not sure I’m really understanding what the core issue is here.

What I think you might be trying to get at is:

class AbstractModel(Model):
    an_fk_field = ForeignKey("Undefined at this point model", ...)

class ConcreteModel(AbstractModel):
   "Something that defines what an_fk_field is referring to"

No, the above cannot be done. (Nor can I think of a situation where there’s any value in terms of reducing the amount of code needed when trying to do so.)

However, the following is valid:

class AbstractModel(Model):
    def model_method(self):
        # Do something with self.an_fk_field

class ConcreteModel(AbstractModel):
    an_fk_field = ForeignKey('OtherModel', ...)

class AnotherConcreteModel(AbstractModel):
    an_fk_field = ForeignKey('ThirdModel', ...)

Keep in mind the following points:

  • Django Models are metaclassed Python Classes. The Django Model class constructs the Python class that represents that model. It’s a subtle distinction that you don’t need to worry about 90+% of the time. For the most part, you can think of a Django Model exactly the same way you think of standard Python classes.

    • This means you have the ability to use the Python hasattr, getattr and setattr functions on those classes to reference fields by a variable name
  • Django Abstract Models are parent classes to this construction process. An abstract class has no existence of its own.

  • Django Models map to database tables. Every model field is a column in a table. What you can do with a Model is effectively limited to what can be represented in the database

    • ForeignKey fields are (usually) mapped as the primary key of the related table, with an index and constraint applied.

So with all this in mind, what I would actually recommend is that you model your database tables first, using the standard techniques for relational database modelling. After you’ve defined your relational model, then work on how that maps to a set of Django Model classes.

1 Like

Thank you, Ken. I think you answered my question despite my unclear phrasing. I do understand that I cannot define foreign key fields on an abstract model. It did not occur to me, though, that I could still reference/do something with self.an_fk_field on AbstractModel.model_method(), even though an_fk_field is only later defined on the concrete model. But now that I think about it, it does make sense. And it hopefully makes what I want to achieve so much easier. I can’t wait to try it out. :slight_smile:

I learned web development (as a hobbyist) when it was more common to define the schema and write SQL queries on your own, so I mostly think of Django models as representations of my database tables. Thinking about which table structure I want to achieve is what led me to my question. Hopefully, alternative 5 instead of alternative 2 - as per Luke’s blog post - turns out to have been a good choice, but I’m quite optimistic now.

I’ll report back once I have a working prototype of the new setup.