Django comes with a complete collection of model fields that you can use to build your models. However, you can also create your own model fields to store custom data or alter the behavior of existing fields.
We need a field that allows us to specify an order for objects. If you think about an easy way to do this with a field provided by Django, you will probably think of adding a PositiveIntegerField
to your models. This is a good starting point. We can create a custom field that inherits from PositiveIntegerField
and provides additional behavior.
There are two relevant functionalities that we will build into our order field:
1
and 2
respectively, when saving a third object, we should automatically assign the order 3
to it if no specific order is given.Create a new fields.py
file inside the courses
application directory and add the following code to it:
from django.db import models from django.core.exceptions import ObjectDoesNotExist class OrderField(models.PositiveIntegerField): def __init__(self, for_fields=None, *args, **kwargs): self.for_fields = for_fields super(OrderField, self).__init__(*args, **kwargs) def pre_save(self, model_instance, add): if getattr(model_instance, self.attname) is None: # no current value try: qs = self.model.objects.all() if self.for_fields: # filter by objects with the same field values # for the fields in "for_fields" query = {field: getattr(model_instance, field) for field in self.for_fields} qs = qs.filter(**query) # get the order of the last item last_item = qs.latest(self.attname) value = last_item.order + 1 except ObjectDoesNotExist: value = 0 setattr(model_instance, self.attname, value) return value else: return super(OrderField, self).pre_save(model_instance, add)
This is our custom OrderField
. It inherits from the PositiveIntegerField
field provided by Django. Our OrderField
field takes an optional for_fields
parameter that allows us to indicate the fields that the order has to be calculated with respect to.
Our field overrides the pre_save()
method of the PositiveIntegerField
field, which is executed before saving the field into the database. In this method, we perform the following actions:
self.attname
, which is the attribute name given to the field in the model. If the attribute's value is different than None
, we calculate the order we should give it as follows:self.model
.for_fields
parameter of the field, if any. By doing so, we calculate the order with respect to the given fields.last_item = qs.latest(self.attname)
from the database. If no object is found, we assume this object is the first one and assign the order 0
to it.1
to the highest order found.setattr()
and return it.You can find more information about writing custom model fields at https://docs.djangoproject.com/en/1.8/howto/custom-model-fields/.
Let's add the new field to our models. Edit the models.py
file of the courses
application and import the new field as follows:
from .fields import OrderField
Then, add the following OrderField
field to the Module
model:
order = OrderField(blank=True, for_fields=['course'])
We name the new field order
, and we specify that the ordering is calculated with respect to the course by setting for_fields=['course']
. This means that the order for a new module will be assigned adding 1
to the last module of the same Course
object. Now you can edit the __str__()
method of the Module
model to include its order as follows:
def __str__(self): return '{}. {}'.format(self.order, self.title)
Module contents also need to follow a particular order. Add an OrderField
field to the Content
model as follows:
order = OrderField(blank=True, for_fields=['module'])
This time, we specify that the order is calculated with respect to the module
field.
Finally, let's add a default ordering for both models. Add the following Meta
class to the Module
and Content
models:
class Meta: ordering = ['order']
The Module
and Content
models should now look as follows:
class Module(models.Model): course = models.ForeignKey(Course, related_name='modules') title = models.CharField(max_length=200) description = models.TextField(blank=True) order = OrderField(blank=True, for_fields=['course']) class Meta: ordering = ['order'] def __str__(self): return '{}. {}'.format(self.order, self.title) class Content(models.Model): module = models.ForeignKey(Module, related_name='contents') content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in':('text', 'video', 'file')}) object_id = models.PositiveIntegerField() item = GenericForeignKey('content_type', 'object_id') order = OrderField(blank=True, for_fields=['module']) class Meta: ordering = ['order']
Let's create a new model migration that reflects the new order fields. Open the shell and run the following command:
python manage.py makemigrations courses
You will see the following output:
You are trying to add a non-nullable field 'order' to content without a default; we can't do that (the database needs something to populate existing rows). Please select a fix: 1) Provide a one-off default now (will be set on all existing rows) 2) Quit, and let me add a default in models.py Select an option:
Django is telling us that since we added a new field for an existing model, we have to provide a default value for existing rows in the database. If the field had null=True
, it would accept null values and Django would create the migration without asking for a default value. We can specify a default value or cancel the migration and add a default
attribute to the order
field in the models.py
file before creating the migration.
Enter 1
and press Enter to provide a default value for existing records. You will see the following output:
Please enter the default value now, as valid Python The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now() >>>
Enter 0
so that this is the default value for existing records and press Enter. Django will ask you for a default value for the Module
model too. Choose the first option and enter 0
as default value again. Finally, you will see an output similar to the following one:
Migrations for 'courses': 0003_auto_20150701_1851.py: - Change Meta options on content - Change Meta options on module - Add field order to content - Add field order to module
Then, apply the new migrations with the following command:
python manage.py migrate
The output of the command will inform you that the migration was successfully applied as follows:
Applying courses.0003_auto_20150701_1851... OK
Let's test our new field. Open the shell using python manage.py shell
and create a new course as follows:
>>> from django.contrib.auth.models import User >>> from courses.models import Subject, Course, Module >>> user = User.objects.latest('id') >>> subject = Subject.objects.latest('id') >>> c1 = Course.objects.create(subject=subject, owner=user, title='Course 1', slug='course1')
We have created a course in the database. Now, let's add modules to the course and see how the modules' order is automatically calculated. We create an initial module and check its order:
>>> m1 = Module.objects.create(course=c1, title='Module 1') >>> m1.order 0
OrderField
sets its value to 0
, since this is the first Module
object created for the given course. Now we create a second module for the same course:
>>> m2 = Module.objects.create(course=c1, title='Module 2') >>> m2.order 1
OrderField
calculates the next order value adding 1
to the highest order for existing objects. Let's create a third module forcing a specific order:
>>> m3 = Module.objects.create(course=c1, title='Module 3', order=5) >>> m3.order 5
If we specify a custom order, the OrderField
field does not interfere and the value given to the order
is used.
Let's add a fourth module:
>>> m4 = Module.objects.create(course=c1, title='Module 4') >>> m4.order 6
The order for this module has been automatically set. Our OrderField
field does not guarantee that all order values are consecutive. However, it respects existing order values and always assigns the next order based on the highest existing order.
Let's create a second course and add a module to it:
>>> c2 = Course.objects.create(subject=subject, title='Course 2', slug='course2', owner=user) >>> m5 = Module.objects.create(course=c2, title='Module 1') >>> m5.order 0
To calculate the new module's order, the field only takes into consideration existing modules that belong to the same course. Since this is the first module of the second course, the resulting order is 0
. This is because we specified for_fields=['course']
in the order
field of the Module
model.
Congratulations! You have successfully created your first custom model field.