You may want to inform yourself about human rights in China.

On a Paste Service (Django Tutorial)

date: 2024-08-13
update: 2024-08-19

The Django tutorial is a great first resource to get a feel for how Django works. However, Django being rather sophisticated, I think it’s best to repeat some of those steps a few times in slightly different settings to help internalize the knowledge.

We build here on said tutorial: for the most part, we repeat very similar steps, encouraging the reader to use his memory over googling around, and also progressively incorporate bits that haven’t been explored in the tutorial.

Our goal is to progressively implement a paste service1, one step at a time. In particular, the project is decomposed in multiple drafts, each one bringing a set of major changes.

We thus assume that you went through the Django tutorial, and have a decent grasp of the basics of programming. As was the case for the tutorial, we won’t bother about deployment here (e.g. WSGI/ASGI), and rely on the development server provided by manage.py.

The code is available on github, one tag per draft, and generally one commit per exercise / chunk:

Wild Rovers movie poster, reproduced by Heritage Auction, gouache on board

Wild Rovers movie poster, reproduced by Heritage Auction, gouache on board by Frank McCarthy through artsy.netHeritage Auction’s “fair use”

Getting ready

# Create a directory and get in
% mkdir django-paste
% cd django-paste

# I'd recommend to version things with git(1). You can version
# things after every exercices for example.
#
# If you're not familiar with git(1), it's definitely the occasion
# to learn how to use it.
% git init

[...]

# Remove some noise
% echo '*__pycache__*' > .gitignore

# Create our project
% django-admin startproject pastesite

# Move in and create our app (remember, a project is
# a collection of apps).
% cd pastesite/
% python manage.py startapp paste

# Move back to our original directory and version things.
% cd -
% git add .gitignore pastesite/
% git commit -m 'creating initial project & app'

[...]

First draft

Here are the features we want to provide by the end of this first draft:

Exercise: I’d recommend you to stop reading here and see how far you can implement this on your own. Try to make a conscious effort to recall what you’ve done over heading back to the docs/code you’ve already wrote. Even if you fail, it’ll still help commit to things to memory.

Exercise: Why do we want pastes to be identified by a string of alphanumeric characters? Why not a regular SQL auto-incremented integer ID?

Solution:[+]

Model

Recall that in Django’s nomenclature, the “models” correspond to Python objects, which are “compiled” by Django to a SQL database and SQL queries allowing to access it; said queries are wrapped by Python objects. For simplicity, we’ll work with SQLite3, which is the default (this avoids us to have to bother with setting up a database).

Exercise: Make sure that your project is indeed configured to work with SQLite3. You may also want to setup the project’s timezone. Finally, you want to register our paste app within our project.

Tip:[+]
Solution:[+]

Exercise: Add a Paste model. Keep is simple: it should be exactly sufficient for our current requirements (e.g. don’t bother adding creation dates or whatnot).

Tip:[+]
Tip:[+]
Tip:[+]
Solution:[+]

You probably want to test it out; in case you haven’t already (you should have…), remember that because we’ve altered our model, we need to

  1. Create a migration:
    % python manage.py makemigrations paste
    Migrations for 'paste':
    paste/migrations/0001_initial.py
      - Create model Paste
  2. Eventually, inspect the SQL corresponding to the migration:
    % python manage.py sqlmigrate paste 0001
    BEGIN;
    --
    -- Create model Paste
    --
    CREATE TABLE "paste_paste" ("id" varchar(50) NOT NULL PRIMARY KEY, "content" text NOT NULL);
    COMMIT;
  3. Execute said migration
    % python manage.py migrate
    Operations to perform:
    Apply all migrations: admin, auth, contenttypes, paste, sessions
    Running migrations:
    Applying contenttypes.0001_initial... OK
    Applying auth.0001_initial... OK
    Applying admin.0001_initial... OK
    
    [...]

Exercise: You should now be able to try to add a few paste entries by hand using the migrate.py shell. Confirm that the database is correctly populated as well.

Solution:[+]

Admin site

Okay, now, let’s quickly recall how to setup the admin site. Let’s first register our model so that it can be accessed within the admin panel:

# That's in pastesite/paste/admin.py
from django.contrib import admin

from .models import Paste

admin.site.register(Paste)

And then:

# We need to create a superuser
% python manage.py createsuperuser
Username (leave blank to use 'mb'): admin
Email address: admin@admin.org
Password:
Password (again):
Superuser created successfully.

# Start the development server
% python manage.py runserver

# And open a browser to http://localhost:8000/admin/
# From there you can eventually create another paste.

Viewing a paste

Exercise: Create a new view which allows to inspect the content of a paste. Remember that this implies:

Solution:[+]

Submitting a paste

Exercise: Create a new “index” view containing a form, allowing to submit a paste. Upon submission, the user should be automatically redirected to the corresponding paste viewing page. Again this implies setting up urls, templates, and writing the views.

Solution:[+]

Second draft: adding dates, preparing the third draft

Adding dates

Let’s start with something that hasn’t been done in the Django tutorial: we have a database, a table to hold our pastes, and we now want to extend our Paste model to include a few new fields: in particular we want to store:

Exercise: How would you naively tackle this? What issue in particular are you expecting? Do you think Django will be able to help? If so, then how? Think about it a little and then give it a shot.

Tip:[+]
Solution:[+]

Exercise: Adjust the paste showing view to display both dates.

Solution:[+]

Tweaking migrations by hand

If you look at the SQL code generated by the previous migration, you’ll see that each column is added one at a time. Let’s pretend that we think it’s a bad thing (e.g. “it’s too slow”) and that we want to tweak that migration to perform both stages at once.

Note: This is “for fun”: the intent is to give you some practice for the day you’ll have to deal with things like this in a more serious setting…

Exercise: Revert our last migration. Try to see if you can replay it. Make sure the migration is reverted before moving on (meaning, if you’ve replayed it, revert it once more).

Tip:[+]
Tip:[+]
Solution:[+]

Exercise: Now, grab/compute the SQL migration code so as to add the two fields in one fell swoop; execute the code, and find a way to “trick” django into thinking that the migration has been successfully applied.

Solution:[+]

Third draft: paste edition, deletion

Let’s add a few more features to our service:

This means we want to have some notion of ownership, but we’d like to keep the service essentially anonymous. A quick way to authentify a user would be to provide the paste creator with a unique “secret” token (to fix the ideas, make it alphanumeric bytes long), which he could later use to justify of his identity.

Of course this isn’t flawless: for example, were the user to lose that token, he’d lose all privileges over the paste. Nevertheless, it’s a reasonable engineering solution for a paste service.

Exercise: Without reading much further, think about it a little, and try to implement those features, in the way that we’ve just discussed, on your own.

We’ll propose below a way to implement this one bit at a time.

Adding a new field; manual update tweaking v2

Exercise: So, we probably should start by adding a new token field to our Paste model; do so, and look at the code of the corresponding SQL migration: what do you see?

Solution:[+]

Exercise: Try to find a way to solve the problem mentioned in the previous solution. Again, it’s not really crucial, but it’s good practice to get a feel for how much we can bend Django migrations.

You can think of this approach as a refinement over the one we took in the previous draft.

Tip:[+]
Solution:[+]

“Sending” the token to the paste owner

Exercise: For practice, let’s first implent this in a clumsy way: once the paste is created in the new() view, we perform a redirect to the paste page; add to this redirection a GET parameter containing the token. What’s the problem with this approach?

Solution:[+]

Exercise: Alright, let’s see if we can do better: essentially, what we try to achieve is have data flow from the new() view to paste(). We could for example store that data somewhere, on the server in new(), and check later in paste() if there’s anything for us.

This is typically implemented with something called “sessions”, of which django provides us with a ready-made implementation. The idea behind sessions is that django will keep some data associated to an user somewhere on the server side, and send that user’s browser a cookie. The user will then send back that cookie at every request: the server can then use that cookie to pick up the data associated to the user.

So, try to use sessions to store the token and send it back to the user. The token must be provided in a new form located in the paste() view, pre-filled with that token, at least upon paste creation. In the next section, we’ll add buttons to that form to edit/delete the paste.

Tip:[+]
Solution:[+]

Editing and removing paste

Exercise: We’re now ready to augment that form – the one containing our ownership token – by adding:

Now, because your actions (deletion/edition) may fail, you’ll also want to find a way to notify the user about an eventual error.

Tip:[+]
Solution:[+]

Again, the final code is available on github.


  1. In case: a paste service is a small website on which usually anonymous users can submit chunks of text to share them. See for example paste.org ↩︎


In the series:


Comments

By email, at mathieu.bivert chez:

email