Unit 2: Views and URLs
I. Orientation: The Django Request–Response Cycle
Django (first released July 2005, by Adrian Holovaty and Simon Willison at the Lawrence Journal-World) follows an MVT pattern — Model, View, Template. Unit 2 concerns the two layers that sit between the browser and the template: the URLconf, which decides which Python function handles an incoming URL, and the view, which decides what that handler returns. Everything below rests on one invariant: a view is a callable that takes an HttpRequest and returns an HttpResponse.
Defining properties and conventions assumed throughout:
- Single entry point:
wsgi.py/asgi.pyreceives the request; middleware processes it;ROOT_URLCONF(set insettings.py, e.g.'mysite.urls') names the module Django consults for routing. - Ordered matching: Django tries each pattern in
urlpatternstop to bottom and stops at the first match — order is significant, not just the pattern. - Leading slash stripped: For
http://example.com/blog/2024/, Django matches againstblog/2024/— the domain, port, GET query string and leading/are excluded. - Explicit is better than implicit: No auto-routing by function name (unlike some frameworks); every view must be listed in a URLconf.
- Stateless HTTP: Each request is independent; continuity comes from cookies, sessions or the database, never from view-level variables.
- Loose coupling: Views never hard-code URLs; URLs never hard-code presentation.
reverse()/{% url %}connect the two by name.
II. Views — Turning a Request into a Response
A. Definition and contract
A view is a Python callable stored by convention in app/views.py.
- Signature:
def view_name(request, *args, **kwargs)—requestis always the first positional argument, anHttpRequestinstance created by Django. - Return obligation: must return an
HttpResponsesubclass, or raise an exception (Http404,PermissionDenied). ReturningNoneraisesValueError: The view didn't return an HttpResponse object. - Two forms: function-based views (FBVs) and class-based views (CBVs), the latter exposed to the URLconf through
.as_view().
B. Creating views and mapping to URLs
Mapping is a two-file operation: write the callable, then register it in a urlpatterns list.
- The view (
blog/views.py):
from django.http import HttpResponse
def index(request):
return HttpResponse("<h1>Blog Home</h1>")- The app URLconf (
blog/urls.py):
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.index, name='index'),
]- Wiring into the project (
mysite/urls.py):include()delegates the remainder of the path to the app.
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('blog/', include('blog.urls')),
]- Arguments of
path():route(the string pattern),view(the callable),kwargs(an optional dict of extra fixed arguments passed to every call),name(the label used for reverse lookup). - Result:
/blog/→blog.views.index. Theblog/prefix is consumed byinclude()and never seen byblog/urls.py.
C. Creating views and view logic
View logic is the controller work: read input, query models, choose a response.
- Canonical four steps: (1) extract parameters from URL kwargs or
request.GET/request.POST; (2) query or mutate models; (3) build a context dictionary; (4) render a template or redirect.
from django.shortcuts import render, get_object_or_404
from .models import Article
def article_detail(request, article_id):
article = get_object_or_404(Article, pk=article_id)
related = Article.objects.filter(topic=article.topic)[:5]
return render(request, 'blog/detail.html',
{'article': article, 'related': related})- Shortcuts that compress logic:
render(request, template, context)= load + render + wrap inHttpResponse;redirect('blog:index')returns a 302;get_object_or_404(Model, **filter)raisesHttp404instead ofDoesNotExist. - Branching on method: the standard POST-handling idiom guards mutation behind
if request.method == 'POST':, and re-renders the empty form otherwise — this prevents GET requests from changing state. - Thin views: business rules belong in model methods or a service module; a view that exceeds ~30 lines is usually doing model work.
D. Function-based versus class-based views
- FBV: explicit, top-to-bottom readable, all branching visible; but repeats boilerplate (pagination, form handling) across views.
- CBV:
class ArticleList(ListView): model = Articlereplaces ~10 lines with configuration; behaviour is inherited through mixins (LoginRequiredMixin), and HTTP methods dispatch toget()/post()automatically — at the cost of hidden control flow spread over the MRO.
III. HTTP Requests and Responses
A. Orientation: the protocol layer
HTTP is a stateless, text-based request/response protocol. Django models each half as an object: HttpRequest (built by Django, never by you) and HttpResponse (built by you).
- Request line: method, path, protocol version —
GET /blog/12/ HTTP/1.1. - Common methods:
GET(safe, idempotent, retrieval),POST(unsafe, creates/mutates),PUT,PATCH,DELETE,HEAD. - Status classes: 2xx success (200 OK, 201 Created), 3xx redirect (301 permanent, 302 found), 4xx client error (400, 403, 404), 5xx server error (500).
B. HTTP requests
HttpRequest exposes the incoming message as attributes, all read-only in normal use.
request.method: the uppercase string'GET','POST', etc.request.GET/request.POST:QueryDictobjects (immutable, multi-value).request.GET.get('page', 1)reads?page=3safely with a default;request.GET.getlist('tag')returns all values for a repeated key.request.path: the full path,'/blog/12/', excluding the query string;request.get_full_path()includes it.request.FILES: uploaded files, populated only when the method is POST and the form hasenctype="multipart/form-data".request.META: raw CGI-style headers dict —HTTP_USER_AGENT,REMOTE_ADDR. Modern equivalent:request.headers['User-Agent'].request.user,request.session,request.COOKIES: attached by middleware, not by HTTP itself —request.userisAnonymousUserif unauthenticated.
C. Creating Requests and Responses
Responses are constructed explicitly; requests are constructed only in tests.
- Plain response:
HttpResponse("text", content_type="text/plain", status=201). Content can be appended after creation withresponse.write(...). - Headers and cookies:
response['Cache-Control'] = 'no-cache';response.set_cookie('theme', 'dark', max_age=3600). - Specialised subclasses:
JsonResponse({'ok': True})(setsapplication/json, serialises withDjangoJSONEncoder);HttpResponseRedirect(url)(302);HttpResponsePermanentRedirect(301);FileResponsefor streamed downloads. - Creating requests (testing):
RequestFactorybuilds a genuineHttpRequestwithout the server:
from django.test import RequestFactory
request = RequestFactory().get('/blog/?page=2')
response = index(request) # call the view directly
assert response.status_code == 200- Outbound requests: calling an external API from a view uses the third-party
requestslibrary — unrelated toHttpRequest, and best kept out of the view body to avoid blocking the response.
IV. The URLconf — Understanding and Designing URL Patterns
A. Understanding URLs
A URL is an addressable resource identifier, and in Django its structure is designed, not derived from file paths.
- Anatomy:
https://example.com:8000/blog/2024/django/?sort=new#top→ scheme, host, port, path (matched), query string (request.GET), fragment (never sent to the server). - Design conventions: nouns not verbs (
/articles/12/not/showArticle?id=12); hierarchical nesting; trailing slash by default, withAPPEND_SLASH = Trueissuing a 301 to add a missing one. - Reverse resolution:
reverse('blog:detail', args=[12])and{% url 'blog:detail' article.id %}generate/blog/12/from the name, so changing a pattern never breaks templates. - Namespacing:
app_name = 'blog'plusinclude()gives the'blog:detail'form, preventing collisions between two apps that both defineindex.
B. Mapping URLs with Params
Captured segments become keyword arguments to the view; the name in angle brackets must equal the view's parameter name.
path('archive/<int:year>/<slug:topic>/', views.archive, name='archive')
def archive(request, year, topic): # year is int 2024, topic is str
...- Built-in path converters:
str(any text without/, the default),int(zero or positive, returnsint),slug(letters, digits, hyphen, underscore),uuid,path(matches text including/). - Type coercion:
<int:year>delivers2024as an integer, so noint()cast in the view — and/archive/abc/simply fails to match, never reaching the view. - Extra fixed kwargs:
path('feed/', views.feed, {'fmt': 'rss'})passesfmt='rss'to every call. - Query parameters contrast:
?sort=newis not captured by routing; it is read fromrequest.GETand is optional by nature, whereas a path parameter is mandatory.
C. Regular expressions in URLs
re_path() is used when the pattern is beyond a converter's expressiveness — a fixed digit count, an alternation, or a case-insensitive match.
from django.urls import re_path
re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>0[1-9]|1[0-2])/$',
views.month_archive),- Anchors:
^start of the remaining path,$end; omitting$makes a prefix match, which is howinclude()patterns are written. - Named groups
(?P<name>...): passed as keyword arguments; unnamed groups(...)are passed positionally and cannot be mixed with named ones. - Common atoms:
\ddigit,\wword character,[-\w]+slug-like,{4}exact repetition,+one-or-more,?optional. - Raw strings: always prefix with
r''so\dis not read as an escape sequence. - No type coercion:
yeararrives as the string'2024'; regex capture returns text only, unlike<int:year>. - Custom converters: subclassing
path_converterwithregex,to_python()andto_url()gives regex power while keepingpath()'s readability and reversibility.
V. Error Handling
A. Orientation: failure as a response
An error in Django is still an HTTP response with a status code; the framework converts specific exceptions into specific codes.
- Exception-to-status mapping:
Http404→ 404,PermissionDenied→ 403,SuspiciousOperation→ 400, any uncaught exception → 500. DEBUG = True: returns the interactive traceback page with local variables, settings and the matched URL patterns.DEBUG = False: returns the plain error template and requiresALLOWED_HOSTSto be populated.
B. Error Handling
Errors are raised in views and rendered by handler views registered in the root URLconf.
- Raising in the view:
from django.http import Http404
from django.core.exceptions import PermissionDenied
def detail(request, pk):
try:
article = Article.objects.get(pk=pk)
except Article.DoesNotExist:
raise Http404("No article with id %s" % pk)
if article.draft and not request.user.is_staff:
raise PermissionDenied- Handler hooks (root
urls.pyonly):handler404,handler500,handler403,handler400, assigned as dotted strings —handler404 = 'mysite.views.page_not_found'. The 404/403/400 handlers receive(request, exception);handler500receives(request)alone and must not depend on context processors that might themselves fail. - Default templates: placing
404.htmland500.htmlat the template root is sufficient — no explicit handler assignment needed. - Status-bearing responses without exceptions:
return HttpResponse(status=204)orHttpResponseNotAllowed(['POST'])(405) where the condition is not exceptional. - Method guards: the
@require_http_methods(["POST"])decorator returns 405 automatically, keeping the check out of the view body. - Logging: with
DEBUG = False, thedjango.requestlogger emits anERRORrecord for every 500 and aWARNINGfor every 404, and mails the traceback toADMINSviaAdminEmailHandler. - Deliberate testing:
/nonexistent/exercises the 404 path; a view that raisesZeroDivisionErrorexercises the 500 path, which cannot be triggered whileDEBUG = True.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →