Prep 05: DX, Sessions and Permissions from MDN Tutorial Parts7-8

Developer Experience & Improving on MDN Django Tutorial Data Model

DX - Developer Experience

“DX” or Developer Experience refers to features of tools for software developers that help them do their job well. In this prep we will introduce several and apply them to our locallibrary.

Django Admin Shell

Django’s Admin command-line utility provides tons of useful features. In this proep, we’ll use its shell to interact with your application. It can be helpful to use the shell as a REPL to confirm that the python you write for django works as you expect.

Let’s use the shell to perform CRUD actions with the models you’ve defined.

  1. launch the django admin shell by running
    python manage.py shell
    
  2. list all the genres in your database
    all_genres = Genre.objects.all()
    all_genres
    
    • Note: the django admin shell automatically imports all models from all INSTALLED_APPS. This is why we didn’t have to explicitly import Genre for this to succeed.
  3. create a new genre called “Food Science”
    food = Genre.objects.create(name="Food Science")
    food.id # this should show you the internal unique id of this new instance of the Genre class
    
  4. again list all the genres (and confirm you see the Food Science genre)
  5. search for the genre that has the name Food Science
    byebyebye = Genre.objects.get(name="Food Science")
    
  6. let’s see that we can delete instances by deleting this new genre
    byebyebye.delete()
    all_genres
    
    • you should see that the model instance’s delete function returned a tuple showing that 1 instance was deleted and what class that instance belonged to.
    • Note: you should see that evaluating all_genres shows the correct remaining set of instances of the Genre class even though we didn’t update it after invoking the instance’s delete(). As the docs explain, the QuerySet class only actually interacts with the database when the object is evaluated. When we earlier evaluated all_genres it actually asked the database for the current situation, but then we we again evaluated allgenres just now, it again communicated with the database automatically.
  7. You can exit the django shell the same way you usually exit the python shell, e.g.
    exit()
    

pyproject.toml

What/Why?

By keeping a list of a project’s dependencies in a pyproject.toml file, you can use source control to track the changes over time, and if you need to run your project in a new environment (e.g. on a different computer of your own, on your team member’s computer, or in the cloud when you deploy your project), you won’t have to go read your diary for all the uv add commands.

How?

Instead of running uv add Django>=6.1 or any other uv add PACKAGE commands directly, you should instead add the package to your pyproject.toml and then run uv sync. Or, you can use uv add PACKAGE which does both in one step.

Create pyproject.toml
  1. create a new file called pyproject.toml in the root of your locallibrary. Note: this file should be a sibling to .venv/, locallibrary/, manage.py, and db.sqlite3
  2. give it this content
    [project]
    name = "locallibrary"
    version = "0.1.0"
    requires-python = ">=3.14"
    dependencies = [
        "Django>=6.1",
    ]
    
Install dependencies from pyproject.toml

The installer (uv) shouldn’t reinstall packages that you already have so if you already have some dependencies installed and then add one new dependency to your pyproject.toml, then tell it to install again, it shouldn’t do redundant installs, but only actually install the new dependency.

To install the dependencies (that you don’t already have) from the pyproject.toml, run:

uv sync

Data Migrations

As first introduced in Part 2 of the MDN Django Tutorial, migrations are used to update the database when we make changes to our models. So new models, removal of models, and changes to models should result in the creation of migrations when we run the makemigrations command.

In addition to creating migrations for changes to the structure of our data model (in db land, they’d say schema changes), it can be helpful to provide necessary instance of our models to our application programmatically (i.e. rather than interactively clicking through a ton of the admin forms or having to write sql queries to do so). Django calls this a data migration.

  1. it’s helpful for django to create the “empty” migration file into which we will write our commands
    uv run manage.py makemigrations --empty catalog
    
    • Note: you should see output that s`hows the path to the newly created migration file.
  2. Open the new file in your editor. You’ll see that there’s not much in there. The main thing that’s useful is that django assumes we might like the new migration to depend on the most recent migration in the same app, which is correct in our case, so don’t modify the dependencies list it created. Instead add an entry to the operations list.
    migrations.RunPython(seed_books),
    
    • Note: The docs on RunPython explain among other things that we can write reversible or irreversible migrations.
  3. Now we need to write the seed_books function we referred to. At the top of your file, after the imports but before the definition of the Migration class begins, define the seed_books function. Why not steal mine this time?
    • Note:
      1. this code looks like code that we could have written in the django shell. Mostly it is! It’s using the model and QuerySet functions like in our exploration of the shell above, but there’s a super cool difference, and it’s subtle but so powerful: instead of importing the models the typical python way (i.e. from catalog.models import Book), we use apps.get_model(). The difference is that apps.get_model is history-aware while the regular import is anachronistic. When we write this migration, it could be that the Book (model) class has a certain structure (or schema) that might not be the same in the future. We would like to write this migration file in a way that it can succeed not only at the time of authoring, but that it will also succeed even if we eventually remove a field from the Book class that we are referencing in this file, or even if Book comes to have a new required field that we aren’t populating in this file.
  4. Add all these objects to our database by migrating
    python manage.py migrate
    
    • there should be output that suggests this was successful, e.g. 11 objects imported automatically
  5. Confirm that the migration worked. In the django shell (do you remember how to get in there?), do
    da = Author.objects.get(first_name="Douglas", last_name="Adams")
    da_books = da.book_set.all()
    da_books
    

MDN Django Tutorial

Improve the Data Model

Look at this ridiculous excerpt from the Book model as created in Part 3 of the tutorial.

...
class Book(models.Model):
    ...
    author = models.ForeignKey('Author', on_delete=models.RESTRICT, null=True)
    # Foreign Key used because book can only have one author, but authors can have multiple books.
    # Author as a string rather than object because it hasn't been declared yet in file.
    ...

Do you see that?!

because book can only have one author, but authors can have multiple books

Admit it. That’s outrageous! Books can totally have multiple authors! Let’s fix it!

  1. create a BookAuthor model that has
    1. a foreign key to Book
    2. a foreign key to Author
    3. an integer field named author_order
  2. update the Book model to (keep author for now and also) have authors. Declare it as a ManyToManyField, but unlike the one you already have for Genre:
    1. use the through parameter to explicitly tell django that the intermediate model is your newly created BookAuthor
    2. use the related_name parameter to tell django that when referencing all their books from an author instance, you want the accessor to be called just books (otherwise it will default to book_set and collide with the existing accessor created by Book’s author foreign key field)
  3. Having added a new model and changed an existing model, tell django to makemigrations and to migrate
  4. now remove the old author field from the Book model and run (ONLY) makemigrations (this will likely fail because we are referencing author from book in admin.py). If it does:
    1. remove the author field from the list_display of BookAdmin
    2. remove the BookInline from AuthorAdmin
    3. run (ONLY) makemigrations
  5. we need to edit this newly generated migration (it should be in locallibrary/catalog/migrations) to record our existing book-author relationships as BookAuthor instances before it discards them!
  6. make a new function before the definition of the Migration class in the new migration file.
    def authorize_books(apps, schema_editor): # "author-ize" get it X-D
        Author = apps.get_model("catalog", "Author")
        Book = apps.get_model("catalog", "Book")
        BookAuthor = apps.get_model("catalog", "BookAuthor")
    
        # TODO add code here to create a new BookAuthor for each book
    
  7. add your new function as the first operation in the list of operations in the Migration class (look back at one of your data migrations if [like me 😅] you don’t remember how)
  8. tell django to migrate
  9. add BookAuthor to your admin site
  10. create a BookAuthorInline and include it on Author and Book
  11. update your templates to do the right thing now that there can be multiple authors
  12. Push your updated code back to the github (classroom) repo
  13. submit the most recent commit URL to the canvas assignment
Last modified September 29, 2026: add diff (6bfd0e7)