Unit 2: Views and URLs

INT253 — Web Development In Python Using Django 9 min read

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.py receives the request; middleware processes it; ROOT_URLCONF (set in settings.py, e.g. 'mysite.urls') names the module Django consults for routing.
  • Ordered matching: Django tries each pattern in urlpatterns top 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 against blog/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)request is always the first positional argument, an HttpRequest instance created by Django.
  • Return obligation: must return an HttpResponse subclass, or raise an exception (Http404, PermissionDenied). Returning None raises ValueError: 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):
PYTHON
from django.http import HttpResponse

def index(request):
    return HttpResponse("<h1>Blog Home</h1>")
  • The app URLconf (blog/urls.py):
PYTHON
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.
PYTHON
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. The blog/ prefix is consumed by include() and never seen by blog/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.
PYTHON
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 in HttpResponse; redirect('blog:index') returns a 302; get_object_or_404(Model, **filter) raises Http404 instead of DoesNotExist.
  • 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

  1. FBV: explicit, top-to-bottom readable, all branching visible; but repeats boilerplate (pagination, form handling) across views.
  2. CBV: class ArticleList(ListView): model = Article replaces ~10 lines with configuration; behaviour is inherited through mixins (LoginRequiredMixin), and HTTP methods dispatch to get()/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: QueryDict objects (immutable, multi-value). request.GET.get('page', 1) reads ?page=3 safely 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 has enctype="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.user is AnonymousUser if 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 with response.write(...).
  • Headers and cookies: response['Cache-Control'] = 'no-cache'; response.set_cookie('theme', 'dark', max_age=3600).
  • Specialised subclasses: JsonResponse({'ok': True}) (sets application/json, serialises with DjangoJSONEncoder); HttpResponseRedirect(url) (302); HttpResponsePermanentRedirect (301); FileResponse for streamed downloads.
  • Creating requests (testing): RequestFactory builds a genuine HttpRequest without the server:
PYTHON
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 requests library — unrelated to HttpRequest, 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, with APPEND_SLASH = True issuing 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' plus include() gives the 'blog:detail' form, preventing collisions between two apps that both define index.

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.

PYTHON
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, returns int), slug (letters, digits, hyphen, underscore), uuid, path (matches text including /).
  • Type coercion: <int:year> delivers 2024 as an integer, so no int() cast in the view — and /archive/abc/ simply fails to match, never reaching the view.
  • Extra fixed kwargs: path('feed/', views.feed, {'fmt': 'rss'}) passes fmt='rss' to every call.
  • Query parameters contrast: ?sort=new is not captured by routing; it is read from request.GET and 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.

PYTHON
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 how include() 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: \d digit, \w word character, [-\w]+ slug-like, {4} exact repetition, + one-or-more, ? optional.
  • Raw strings: always prefix with r'' so \d is not read as an escape sequence.
  • No type coercion: year arrives as the string '2024'; regex capture returns text only, unlike <int:year>.
  • Custom converters: subclassing path_converter with regex, to_python() and to_url() gives regex power while keeping path()'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 requires ALLOWED_HOSTS to be populated.

B. Error Handling

Errors are raised in views and rendered by handler views registered in the root URLconf.

  • Raising in the view:
PYTHON
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.py only): handler404, handler500, handler403, handler400, assigned as dotted strings — handler404 = 'mysite.views.page_not_found'. The 404/403/400 handlers receive (request, exception); handler500 receives (request) alone and must not depend on context processors that might themselves fail.
  • Default templates: placing 404.html and 500.html at the template root is sufficient — no explicit handler assignment needed.
  • Status-bearing responses without exceptions: return HttpResponse(status=204) or HttpResponseNotAllowed(['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, the django.request logger emits an ERROR record for every 500 and a WARNING for every 404, and mails the traceback to ADMINS via AdminEmailHandler.
  • Deliberate testing: /nonexistent/ exercises the 404 path; a view that raises ZeroDivisionError exercises the 500 path, which cannot be triggered while DEBUG = True.