We continue with our Python Django Tutorial series, this lecture we focus on creating complex Django models and model forms with custom HTML and SlugField. In addition we will be learning how to override the save method in a Django Model – in order to add in some code that will be run before the model is saved each time.

We will assume you have done the Django basics, installed a Django project and app to work from. The model we will be creating will be similar to a user profile Model – which has a one to one relationship with the User class.
If you need to get started with Django – Read our previous tutorial on Django: Creating a Django project from scratch.
Project Background – Job Search Website
We are building a job search website – users who create accounts, will have to start by uploading their Resume. However, we don’t want users to just upload a resume document – we want them to enter details in a Model. This will enable us to structure the data, so we can use it to make queries – like “search for all users with 5years of experience” or “search for all engineers”.
This is why a lot of job search platforms will always require you to re-enter your resume in their own forms. We are going to do the same. We will then be creating a model called Resume. In addition, we will create two extra models (not in this tutorial) called Education and Experience.
Create the Resume Model – Complex Django Model
The Resume Model needs to have the following fields:
- The user associated with the Resume. This will be a OnetoOne Field – so one user can have just one profile and visa versa.
- Unique ID – a unique string we will generate to differentiate the users from each other
- A profile image
- A boolean field to tell us whether the user confirmed their email address or not
- Date of birth
- Ethnicity
- Marital status
- Sex
- Address
- Phone number
- The Slug – a field used to generate an absolute path to the detailed page of an instance of the model
- Date created
- Date last updated
- Cover letter – File Field
- CV or actual Resume document – File Field
There are two additional classes we will add – on top of the usual string method. We need the: (1) Absolute path method and (2) override the save method.
The final file will look like this:
from django.db import models
from django.template.defaultfilters import slugify
from django.contrib.auth.models import User
from django.utils import timezone
from uuid import uuid4
import random
class Resume(models.Model):
BLACK = 'Black'
WHITE = 'White'
COLOURED = 'Coloured'
INDIAN = 'Indian'
CHINESE = 'Chinese'
MALE = 'Male'
FEMALE = 'Female'
OTHER = 'Other'
MARRIED = 'Married'
SINGLE = 'Single'
WIDOWED = 'Widowed'
DIVORCED = 'Divorced'
GAUTENG = 'Gauteng'
MPUMALANGA = 'Mpumalanga'
FREE_STATE = 'Free-state'
NORTH_WEST = 'North-west'
LIMPOPO = 'Limpopo'
WESTERN_CAPE = 'Western-cape'
NOTHERN_CAPE = 'Nothern-cape'
EASTERN_CAPE = 'Eastern-cape'
KWAZULU_NATAL = 'Kwazulu-natal'
ETHNIC_CHOICES = [
(BLACK, 'Black'),
(WHITE, 'White'),
(COLOURED, 'Coloured'),
(INDIAN, 'Indian'),
(CHINESE, 'Chinese'),
]
SEX_CHOICES = [
(MALE, 'Male'),
(FEMALE, 'Female'),
(OTHER, 'Other'),
]
MARITAL_CHOICES = [
(MARRIED, 'Married'),
(SINGLE, 'Single'),
(WIDOWED, 'Widowed'),
(DIVORCED, 'Divorced'),
]
PROVINCE_CHOICES = [
(GAUTENG, 'Gauteng'),
(MPUMALANGA, 'Mpumalanga'),
(FREE_STATE, 'Free-state'),
(NORTH_WEST, 'North-west'),
(LIMPOPO, 'Limpopo'),
(WESTERN_CAPE, 'Western-cape'),
(NOTHERN_CAPE, 'Nothern-cape'),
(EASTERN_CAPE, 'Eastern-cape'),
(KWAZULU_NATAL, 'Kwazulu-natal'),
]
IMAGES = [
'profile1.jpg', 'profile2.jpg', 'profile3.jpg', 'profile4.jpg', 'profile5.jpg',
'profile6.jpg', 'profile7.jpg', 'profile8.jpg', 'profile9.jpg', 'profile10.jpg',
]
user = models.OneToOneField(User, on_delete = models.CASCADE)
uniqueId = models.CharField(null=True, blank=True, max_length=100)
image = models.ImageField(default='default.jpg', upload_to='profile_images')
email_confirmed = models.BooleanField(default=False)
date_birth = models.DateField(blank=True, null=True)
ethnicity = models.CharField(choices=ETHNIC_CHOICES, default=BLACK, max_length=100)
sex = models.CharField(choices=SEX_CHOICES, default=OTHER, max_length=100)
marital_status = models.CharField(choices=MARITAL_CHOICES, default=SINGLE, max_length=100)
addressLine1 = models.CharField(null=True, blank=True, max_length=200)
addressLine2 = models.CharField(null=True, blank=True, max_length=200)
suburb = models.CharField(null=True, blank=True, max_length=100)
city = models.CharField(null=True, blank=True, max_length=100)
province = models.CharField(choices=PROVINCE_CHOICES, default=GAUTENG, max_length=100)
phoneNumber = models.CharField(null=True, blank=True, max_length=100)
slug = models.SlugField(max_length=500, unique=True, blank=True, null=True)
date_created = models.DateTimeField(default = timezone.now)
last_updated = models.DateTimeField(blank=True, null=True)
cover_letter = models.FileField(upload_to='resumes', null=True, blank=True,)
cv = models.FileField(upload_to='resumes', null=True, blank=True,)
def __str__(self):
return '{} {} {}'.format(self.user.first_name, self.user.last_name, self.uniqueId)
def get_absolute_url(self):
return reverse('resume-detail', kwargs={'slug': self.slug})
def save(self, *args, **kwargs):
#Creating a unique Identifier for the resume(useful for other things in future)
if self.uniqueId is None:
self.uniqueId = str(uuid4()).split('-')[0]
self.slug = slugify('{} {} {}'.format(self.user.first_name, self.user.last_name, self.uniqueId))
#assign a default profile image
if self.image == 'default.jpg':
self.image = random.choice(self.IMAGES)
#keep track of every-time someone updates the resume, every-time the instance is saved - this should update
self.slug = slugify('{} {} {}'.format(self.user.first_name, self.user.last_name, self.uniqueId))
super(Resume, self).save(*args, **kwargs)
We have created Select Fields – using CharField with choices defined. Choices are defined as tuples of the actual value and display value.
Model Forms with custom HTML

When it comes to the Form class, there are a couple of steps:
Step 1: All the imports
from django import forms
from django.contrib.auth.models import User
from .models import Resume
Step 2: Modify the DateInput widget
The classic DateInput widget – renders a TextInput and we don’t want that. Because users will enter the date in all types of formats. We want them to select a date from a calendar type widget.
class DateInput(forms.DateInput):
input_type = 'date'
Step 3: The Detailed Model Form with custom HTML and Styling
class ResumeForm(forms.ModelForm):
BLACK = 'Black'
WHITE = 'White'
COLOURED = 'Coloured'
INDIAN = 'Indian'
CHINESE = 'Chinese'
MALE = 'Male'
FEMALE = 'Female'
OTHER = 'Other'
MARRIED = 'Married'
SINGLE = 'Single'
WIDOWED = 'Widowed'
DIVORCED = 'Divorced'
GAUTENG = 'Gauteng'
MPUMALANGA = 'Mpumalanga'
FREE_STATE = 'Free-state'
NORTH_WEST = 'North-west'
LIMPOPO = 'Limpopo'
WESTERN_CAPE = 'Western-cape'
NOTHERN_CAPE = 'Nothern-cape'
EASTERN_CAPE = 'Eastern-cape'
KWAZULU_NATAL = 'Kwazulu-natal'
ETHNIC_CHOICES = [
(BLACK, 'Black'),
(WHITE, 'White'),
(COLOURED, 'Coloured'),
(INDIAN, 'Indian'),
(CHINESE, 'Chinese'),
]
SEX_CHOICES = [
(MALE, 'Male'),
(FEMALE, 'Female'),
(OTHER, 'Other'),
]
MARITAL_CHOICES = [
(MARRIED, 'Married'),
(SINGLE, 'Single'),
(WIDOWED, 'Widowed'),
(DIVORCED, 'Divorced'),
]
PROVINCE_CHOICES = [
(GAUTENG, 'Gauteng'),
(MPUMALANGA, 'Mpumalanga'),
(FREE_STATE, 'Free-state'),
(NORTH_WEST, 'North-west'),
(LIMPOPO, 'Limpopo'),
(WESTERN_CAPE, 'Western-cape'),
(NOTHERN_CAPE, 'Nothern-cape'),
(EASTERN_CAPE, 'Eastern-cape'),
(KWAZULU_NATAL, 'Kwazulu-natal'),
]
image = forms.ImageField(
required=False,
widget=forms.FileInput(attrs={'class': 'form-control'})
)
date_birth = forms.DateField(
required = True,
# input_formats=['%d-%m-%Y'],
widget=DateInput(attrs={'class': 'form-control', 'placeholder': 'Enter a date: '}),
)
ethnicity = forms.ChoiceField(
choices = ETHNIC_CHOICES,
widget=forms.Select(attrs={'class': 'nice-select rounded'}),
)
sex = forms.ChoiceField(
choices = SEX_CHOICES,
widget=forms.Select(attrs={'class': 'nice-select rounded'}),
)
marital_status = forms.ChoiceField(
choices = MARITAL_CHOICES,
widget=forms.Select(attrs={'class': 'nice-select rounded'}),
)
addressLine1 = forms.CharField(
required = True,
widget=forms.TextInput(attrs={'class': 'form-control resume', 'placeholder': 'Enter Address Line 1'}),
)
addressLine2 = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'class': 'form-control resume', 'placeholder': 'Enter Address Line 2'}),
)
suburb = forms.CharField(
required=True,
widget=forms.TextInput(attrs={'class': 'form-control resume', 'placeholder': 'Enter Suburb'}),
)
city = forms.CharField(
required = True,
widget=forms.TextInput(attrs={'class': 'form-control resume', 'placeholder': 'Enter City'}),
)
province = forms.ChoiceField(
choices = PROVINCE_CHOICES,
widget=forms.Select(attrs={'class': 'nice-select rounded'}),
)
phoneNumber = forms.CharField(
required = True,
widget=forms.TextInput(attrs={'class': 'form-control resume', 'placeholder': 'Enter Phone Number'}),
)
cover_letter = forms.FileField(
required=False,
widget=forms.FileInput(attrs={'class': 'form-control'})
)
cv = forms.FileField(
required=False,
widget=forms.FileInput(attrs={'class': 'form-control'})
)
class Meta:
model = Resume
fields = [
'image',
'date_birth',
'ethnicity',
'sex',
'marital_status',
'addressLine1',
'addressLine2',
'suburb',
'city',
'province',
'phoneNumber',
'cover_letter',
'cv',
]
Note how we code the Select Fields with the choices in the widget.
For more details on Django Widgets – read more from the documentation page: https://docs.djangoproject.com/en/3.1/ref/forms/widgets/
When you define the variable, make sure you clearly specify:
required = False,
for fields that are optional. You must do this or it will default to True.
Then all the fields must be rendered on the HTML. We will do it manually, so we must remember to render the errors as well, like so:
<div class="form-group app-label">
<label class="text-muted">Address Line 1<span class="text-danger">*</span> :</label>
{{form.addressLine1}}
</div>
{% if form.addressLine1.errors %}
{% for error in form.addressLine1.errors %}
<div class="alert alert-danger">
<strong>{{ error|escape }}</strong>
</div>
{% endfor %}
{% endif %}
When you render the form in this way, you have the flexibility of styling your form as you wish. But it is more work – you must remember every field, and type in every field one at a time.
Creating Complex Django Models and Model Forms with Custom HTML and SlugField – Video Tutorial
Watch this 3hr long video tutorial on Youtube.

Leave a Reply