Django: detail view must be called with pk or slug
Learn how to use UUID as URLs in your Django projects.
Welcome back to another episode of my Django mini-tutorials!
Lately I've been experimenting with UUID as public identifiers in my Django URLs, an approach suggested in Two Scoops of Django, which incidentally I recall also having read from REST in practice, an old book from 2010.
This technique consists of URLs made out from opaque identifiers, such as random numbers, or better, UUID. The goal is to obscure the model's primary key in your URLs.
Opaque URLs in Django
Let's see opaque URLs in practice.
First off, in the templates you build your links as follows:
// IMAGINE A FOR LOOP!
<a href="{% url "ticket-detail" ticket.uuid %}">{{ ticket.subject }}</a>
<a href="{% url "ticket-detail" ticket.uuid %}">{{ ticket.subject }}</a>
This template can be served from a ListView
for example, to render a list of models.
Here ticket-detail
is a named Django view, configured in URLconf as follows:
urlpatterns = [
path(
"tickets/<uuid:uuid>/",
TicketDetail.as_view(),
name="ticket-detail",
),
]
As a path for the view we accept the uuid
argument.
The uuid
field must be present in the model:
class Ticket(models.Model):
uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
# Other fields ...
This makes possible to serve URLs like /tickets/a3c99176-31e8-4e69-87f3-122f2fe4022f/
which might not be the friendliest URL ever, but it helps to obscure the model's primary key in your URLs.
UUID and Django's DetailView
To render a single model when the user clicks on a link like /tickets/a3c99176-31e8-4e69-87f3-122f2fe4022f/
we can use a DetailView
, which conveniently takes just the model as an attribute:
class TicketDetail(DetailView):
model = Ticket
The problem here is that DetailView
doesn't absolutely know how to fetch the right object from the database based on the UUID passed in the URL.
In fact, if we try to visit something like /tickets/a3c99176-31e8-4e69-87f3-122f2fe4022f/
, Django can't do anything than scream back:
Generic detail view must be called with either an object_pk or a slug in the URLConf
This exception is raised by get_object()
in SingleObjectMixin which expects either slug
or pk
as arguments for the descendant generic view.
PROTIP: It's likely that this functionality is required in more than one view of your project. To keep things clean, you can make your own mixin SingleObjectSlugMixin
by subclassing SingleObjectMixin
.
To make DetailView
happy again we can override get_object()
so that it gets the desired object from the database, starting from the uuid
:
class TicketDetail(DetailView):
model = Ticket
def get_object(self, queryset=None):
return Ticket.objects.get(uuid=self.kwargs.get("uuid"))
Thanks for reading!