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. ) 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!