• 12 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 7/20/22

Capture Data With Models and Fields

Encapsulate an Entity With a Model

In Part 2, we created our first model, the Band  model, with a name  field. This field was of a specific type:  CharField , which is short for character field - a field that holds character data, or string data. 

Our  Band  model could additionally hold data about many other characteristics:

  • Genre

  • Biography

  • year_formed

  • Active (is the band currently active - yes or no?)

  • official_homepage 

What data type could we use for each of these fields? Think about it before adding them in the next section.

We may also want to apply some rules to these fields. For example, what’s the maximum length of characters in a biography? Should genre be a “free text” field, or should we make the user pick from a list of choices? If the band has no official homepage, can we leave that field empty?

In this chapter, you will learn about the different types of fields that a model can have. You’ll also see how to apply rules to these fields to constrain their values within limits you are happy with.

Define Fields in a Model

Let’s go back to our  Band  model, which we left looking like this: 

# listings/models.py

class Band(models.Model):
    name = models.fields.CharField(max_length=100)

Add the additional fields we listed above:

# listings/models.py

class Band(models.Model):
    name = models.fields.CharField(max_length=100)
    genre = models.fields.CharField()
    biography = models.fields.CharField()
    year_formed = models.fields.IntegerField()
    active = models.fields.BooleanField()
    official_homepage = models.fields.URLField()

Let’s consider the field type we’ve used for each field:

  • genre  and  biography : like name , these fields will contain character or string data, so we’ll use CharField .

  • year_formed : a year is essentially an integer, so I’ve opted for an IntegerField  here. Django also has a DateField , but that would include a month and day too, which would be unused in this case.

  • active : this is a “yes” or “no” answer, so a  BooleanField  with True  or False  values is perfect here.

  • official_homepage: we could have used another CharField  here, but Django has a better choice: URLField , which will only allow valid URLs in this field - we’ll see how that works later. 

Pass Arguments to Each Field

Now that we know which field types we’re using, let’s pass arguments (also known as field options) to each field:

# listings/models.py

from django.core.validators import MaxValueValidator, MinValueValidator
...

    class Band(models.Model):
    name = models.fields.CharField(max_length=100)
    genre = models.fields.CharField(max_length=50)
    biography = models.fields.CharField(max_length=1000)
    year_formed = models.fields.IntegerField(
        validators=[MinValueValidator(1900), MaxValueValidator(2021)]
    )
    active = models.fields.BooleanField(default=True)
    official_homepage = models.fields.URLField(null=True, blank=True)
  •  For CharField s, the max_length  option is required - we would get an error if we tried to run the application without it. We’ve chosen appropriate values for each field.

  • year_formed  should have appropriate minimum and maximum values. I’ve opted for a minimum of1900  (We can change this later if we really need to add a band that’s older than that!). The maximum is set to  2021  - that will do for the year in which I’m writing this, but what happens next year? We might need to revisit this later.
    To enforce these constraints, we pass a list of validators to the  validators  option. In this example, we use two of Django’s built-in validator classes:  MinValueValidator  and  MaxValueValidator  , which we import from  django.core.validators  .

  • For  active  , we’ve set a  default  of  True  . So if we failed to specify the active status of a band, it would fall back to  True  .

  • Remember we said we might want to leave the  official_hompage  empty if a band didn’t have one? We can use the option  null=True  to allow NULL values in the database. And when we build a form to create or edit  Band  objects, setting  blank=True  here will allow us to submit that form with an empty textbox for this field. 

Let’s make one more customization to this model. We said we might like to have the  genre  field be limited to a list of specified choices. One problem that free text fields have is that different users can type in multiple versions of the same value - e.g., “Hip Hop,” “hip hop,” and “Hip-Hop.” We can eliminate these variants and keep genres consistent by making the user choose from a list. The downside is that we would need to maintain this list and add new genres from time to time - but for the purposes of our first Django web app, let’s accept that.

Add this code to our models file:

# listings/models.py

class Band(models.Model):

    class Genre(models.TextChoices):
        HIP_HOP = 'HH'
        SYNTH_POP = 'SP'
        ALTERNATIVE_ROCK = 'AR'

    ...
    genre = models.fields.CharField(choices=Genre.choices, max_length=5)
    ...

 We’ve created the  Genre  class, defining the choices that may be used for the  genre  field.  Genre  is a nested class - defined within another class. Sometimes you do this in Python if the classes are very closely related, as they are here.

  • The  Genre  class inherits from  models.TextChoices  - that’s a class in Django that’s designed for defining a list of choices.

  • In  Genre , add a constant for each genre choice we want to allow, (e.g.,  HIP_HOP  ).

  • For each constant we define a key (  ’HH’  ,  ’SP’  ,  ’AR’  ) - this is the value that will be stored in the database for that genre. 

Finally, we update the options for our  genre  field: set  choices=Genre.choices  to limit the choices to those defined in the nested class. So far, our keys are only two characters long (e.g.,  ’HH’  ), but let’s give ourselves some headroom in case the list grows, and set  max_length=5  .

Try the New Model

It’s time to try our new model! Open up the Django shell, and let’s try to save a new  Band  object: 

(env) ~/projects/django-web-app/merchex
→ python manage.py shell
>>> from listings.models import Band
>>> band = Band()
>>> band.save()
...
django.db.utils.OperationalError: table listings_band has no column named genre

 Oh! We can’t save a new  Band  . 

There’s a problem with the table  listings_band  . 

There’s no column named  genre  yet. 

Can you remember what we need to do first?

Add New Model Fields to the Database Schema With a Migration

The problem is that while we have added new fields to our model, we haven't updated our database’s schema - its structure - accordingly.

So let’s do this with a migration. But before we start, here is a quick explanation in video oh what we will do!

Ready to dig in?

The  makemigrations  command will scan our models.py file and figure out what has changed since we last ran the command in Part 2. It will notice that we have added new fields to our  Band  model, and generate a migration that will update our schema with new columns.

Hit  Ctrl-D  to exit the shell, and then:

(env) ~/projects/django-web-app/merchex
→ python manage.py makemigrations
You are trying to add a non-nullable field 'biography' to band without a default; we can't do that (the database needs something to populate existing rows).

We’ll now be prompted for some default values. Django will sometimes ask for these when it makes a migration, and some of the fields are “non-nullable” (in other words, not optional).

Think about it like this: we’ve already got three rows in this table:

id

name

1

De La Soul

2

Cut Copy

3

Foo Fighters

If we’re going to add new columns, and those columns’ values cannot be NULL, then we must put something in those columns for the existing rows (marked with ‘?’): 

id

name

biography

genre

year_formed

active

official_homepage

1

De La Soul

?

?

?

True

NULL

2

Cut Copy

?

?

?

True

NULL

3

Foo Fighters

?

?

?

True

NULL

As Django prompts for each of these fields, in turn, select option 1 (“Provide a one-off default now”), and then use these defaults:

  • biography  : we can pass an empty string here:  ''  .

  • genre  : this should be one of the keys we defined earlier. Let’s use  ’HH’  - sure, not all of our bands are Hip Hop, but we can fix this in the next chapter.

  • year_formed  : let’s use a default of the year  2000  - again, this isn’t true for our bands, but we’ll fix this later. 

Here’s the terminal output for the first field,  biography  . 

(env) ~/projects/django-web-app/merchex
→ python manage.py makemigrations
You are trying to add a non-nullable field 'biography' to band 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 with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 1
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
Type 'exit' to exit this prompt
>>> ''

You’ll then be prompted for the other two fields.

Finally, our migration has been generated:

# Migrations for 'listings':

 listings/migrations/0003_auto_20210329_2350.py
    - Add field active to band
    - Add field biography to band
    - Add field genre to band
    - Add field official_homepage to band
    - Add field year_formed to band

Let’s run it:

(env) ~/projects/django-web-app/merchex
→ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, listings, sessions
Running migrations:
  Applying listings.0003_auto_20210329_2350... OK

Now let’s go back into the shell to try out our model once more:

(env) ~/projects/django-web-app/merchex
→ python manage.py shell
>>> from listings.models import Band
>>> band = Band()
>>> band.save()
...
django.db.utils.IntegrityError: NOT NULL constraint failed: listings_band.year_formed

We got an  IntegrityError  on the field  year_formed . This is a good thing! We didn’t specify  null=True  on that field, so it will error on NULL values. 

You can see that  year_formed  is currently set to  None  : 

>>> band.year_formed is None
True

None  values will besaved as NULL to the database. So let’s set  year_formed  to something: 

>>> band.year_formed = 2000

And now let’s try saving it again:

>>> band.save()
>>> band
<Band: Band object (4)>

This time it worked, and the object was saved to the database.

But what about the other fields like  name  ,  genre  , and  biography  ? We didn’t specify  null=True  on those either, so how come they could be saved to the database? 

Let’s look at the values of those fields:

>>> band.name
''
>>> band.genre
''
>>> band.biography
''

Django has set them to empty strings. These are not NULLs, so it was possible for this band to be inserted into the database.

But is that really what we wanted? What if we wanted to force users to enter a value? 

You’ll see how form validation forces the user to enter a value for these fields in the next chapter.

In the meantime, let’s delete this nameless band from our database:

>>> band.delete()
(1, {'listings.Band': 1})

The shell confirms that one  Band  object was deleted. 

And finally, close the shell with Ctrl-D.

Watch the screencast to go back over anything you're unsure of.

Now You Try! Define More Fields in Your Model, and Make and Run a Migration

Add the following fields to your  Listing  model, thinking about which data type you should use, and which type of field you should use for each one (  CharField  ,  IntegerField  , etc.):

  • description  - describe the item being sold.

  • sold  - has it sold yet or not? What should be the default value for a new listing?

  • year  - what year does the item originate from? This will be useful to buyers searching for vintage items. But we should be able to leave this field empty if the year is unknown.

  • type  - there are a few “types” of listing: Records, Clothing, Posters, and Miscellaneous. 

Then make a migration. Remember you’ll be prompted for default values for any non-nullable fields. Don’t worry if the default you choose isn’t strictly correct for your listings objects - we’ll fix that in the next chapter.

Finally, apply your migration. We’ll see the new fields in action in the next chapter.

Let’s Recap!

  • Django comes with different field types that map to different data types, like  CharField  and  IntegerField  . There are also more specific fields that will constrain the input, like  URLField  .

  • You can define constraints and rules for fields by setting options on them like  max_length  ,  null  , and  choices  .

  • You can fine-tune constraints on fields even further by specifying validators on fields using the  validators  option.

  • When you add new fields to a model, you have to make a migration to add new columns to the database before you can start using them.

  • If you add non-nullable fields to a model, you’ll be prompted to provide an initial default for them when you make a migration. 

Now that we’ve built out our models, let’s learn about an interface we can use to manage them that’s built right into Django - the admin site.

Example of certificate of achievement
Example of certificate of achievement