Prep 05: DX, Sessions and Permissions from MDN Tutorial Parts7-8
Categories:
9 minute read
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.
- launch the django admin shell by running
python manage.py shell - 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 importGenrefor this to succeed.
- Note: the django admin shell automatically imports all models from all
- 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- Note: we could have used the Genre class’s constructor here, but that would require a second line of code to
savethe newGenreinstance (so that it would not only exist in memory, but also would be persisted to the database.)
- Note: we could have used the Genre class’s constructor here, but that would require a second line of code to
- again list all the genres (and confirm you see the
Food Sciencegenre) - search for the genre that has the name
Food Sciencebyebyebye = Genre.objects.get(name="Food Science")- Note: we can search in other ways such as with
filter. Many of the interactions we’ll have like this are with Django’s QuerySet objects and many of its methods expect a certain field lookup syntax
- Note: we can search in other ways such as with
- 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_genresshows the correct remaining set of instances of theGenreclass even though we didn’t update it after invoking the instance’sdelete(). As the docs explain, theQuerySetclass only actually interacts with the database when the object is evaluated. When we earlier evaluatedall_genresit actually asked the database for the current situation, but then we we again evaluatedallgenresjust now, it again communicated with the database automatically.
- 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
In our course, this is likely unnecessary because you already ran a uv init command
- create a new file called
pyproject.tomlin the root of your locallibrary. Note: this file should be a sibling to.venv/,locallibrary/,manage.py, anddb.sqlite3 - 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
When working with other developers, this is a command you may need to run after you get changes from them.
- How will you know whether you need to run it again?
- Are there any other steps you might need to take after receiving code from your teammates?
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.
When working with other developers, this is a command you may need to run after you get changes from them.
- How will you know whether you need to run it again?
- Are there any other steps you might need to take after receiving code from your teammates?
These next few steps are helpful for your future work, but this semester you likely already have the data below from doing the data migration in class.
- 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.
- 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
dependencieslist it created. Instead add an entry to theoperationslist.migrations.RunPython(seed_books),- Note: The docs on
RunPythonexplain among other things that we can write reversible or irreversible migrations.
- Note: The docs on
- Now we need to write the
seed_booksfunction we referred to. At the top of your file, after the imports but before the definition of theMigrationclass begins, define theseed_booksfunction. Why not steal mine this time?- Note:
- 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 useapps.get_model(). The difference is thatapps.get_modelishistory-awarewhile the regularimportis 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 ifBookcomes to have a new required field that we aren’t populating in this file.
- 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.
- Note:
- 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
- there should be output that suggests this was successful, e.g.
- 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- You should see that
da_booksis aQuerySetthat has (at least) theBooktitledHitchhiker's Guide to the Galaxy. - BUT WAIT! search your whole project for
book_set- in vs code you can do: Ctrl+Shift+F (or Cmd+Shift+F on a mac)
- we never defined a property of the
Authormodel calledbook_set! Why did it work?! one thing that you may not have noticed when you completed Part 6 of the MDN Django tutorial is illustrated here: when using django field types that create relationships between model classes, django’s default behavior is to create accessor fields in both directions. So we explicitly defined theauthorfield on theBookmodel and django automatically created a convenience accessor for us to follow the relationship in the opposite direction (i.e. when we have an instance of an author, we can refer to all their books). We can customize or even prevent this behavior if we wish/require.
- You should see that
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!
- create a
BookAuthormodel that has- a foreign key to
Book - a foreign key to
Author - an integer field named
author_order
- a foreign key to
- update the
Bookmodel to (keepauthorfor now and also) haveauthors. Declare it as aManyToManyField, but unlike the one you already have forGenre:- use the
throughparameter to explicitly tell django that the intermediate model is your newly createdBookAuthor - use the
related_nameparameter to tell django that when referencing all their books from anauthorinstance, you want the accessor to be called justbooks(otherwise it will default tobook_setand collide with the existing accessor created byBook’sauthorforeign key field)
- use the
- Having added a new model and changed an existing model, tell django to
makemigrationsand tomigrate - now remove the old
authorfield from theBookmodel and run (ONLY)makemigrations(this will likely fail because we are referencingauthorfrombookinadmin.py). If it does:- remove the
authorfield from thelist_displayofBookAdmin - remove the
BookInlinefromAuthorAdmin - run (ONLY)
makemigrations
- remove the
- we need to edit this newly generated migration (it should be in
locallibrary/catalog/migrations) to record our existing book-author relationships asBookAuthorinstances before it discards them! - make a new function before the definition of the
Migrationclass 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 - add your new function as the first operation in the list of
operationsin theMigrationclass (look back at one of your data migrations if [like me 😅] you don’t remember how) - tell django to
migrate - add
BookAuthorto your admin site - create a
BookAuthorInlineand include it onAuthorandBook - update your templates to do the right thing now that there can be multiple authors
- Push your updated code back to the github (classroom) repo
- submit the most recent commit URL to the canvas assignment