Function-Based Views
Every URL pattern in TaskFlow points at a view — and in its simplest form, a view is nothing more exotic than a Python function. This lesson works through the handful of things a function-based view typically needs to do: build a response from a template, look up a single object safely, react differently depending on the HTTP method, and redirect after a successful action instead of re-rendering the same page.
A View Is Just a Function
A function-based view takes an HttpRequest object (conventionally named
request) as its first argument, plus one parameter for every value captured by the URL
pattern, and it must return something that behaves like an HttpResponse. That's the
entire contract — nothing about classes, decorators, or inheritance is required to get started.
# taskflow/views.py
from django.http import HttpResponse
def ping(request):
return HttpResponse('TaskFlow is running.')
That's a complete, valid Django view. In practice, almost every real view hands its work off to a
shortcut instead of building an HttpResponse by hand — starting with render().
The render() Shortcut
render() takes the current request, a template path, and a dictionary of
context — the data the template is allowed to use — and returns a finished
HttpResponse with the template rendered against that context.
# taskflow/views.py
from django.shortcuts import render
def task_list(request):
tasks = Task.objects.all().order_by('due_date')
context = {'tasks': tasks}
return render(request, 'taskflow/task_list.html', context)
Inside taskflow/task_list.html, the queryset assigned to context['tasks']
becomes available as the variable tasks — that connection between view and template is
covered from the template side in Django Template
Language.
Fetching a Single Object with get_object_or_404()
Looking up a task by primary key with Task.objects.get(pk=task_id) works, but it
raises Task.DoesNotExist — an unhandled exception, and an ugly server error — the
moment someone requests a task ID that isn't there. get_object_or_404() wraps the same
lookup and turns a missing row into a clean, expected 404 response instead.
# taskflow/views.py
from django.shortcuts import render, get_object_or_404
def task_detail(request, task_id):
task = get_object_or_404(Task, pk=task_id)
return render(request, 'taskflow/task_detail.html', {'task': task})
The first argument is a model (or a queryset), and any further arguments are passed straight
through as lookup filters — so get_object_or_404(Task, pk=task_id, project__owner=request.user)
works too, and 404s if the task exists but doesn't belong to that project's owner.
Branching on request.method
A single URL is often used for both showing a page and acting on a submission from that page. The
standard way to tell those two cases apart inside one view is to check
request.method. Here's a "mark task complete" toggle handled entirely in one function:
# taskflow/views.py
from django.shortcuts import render, get_object_or_404, redirect
def toggle_task_complete(request, task_id):
task = get_object_or_404(Task, pk=task_id)
if request.method == 'POST':
task.completed = not task.completed
task.save()
return redirect('task-detail', task_id=task.id)
return render(request, 'taskflow/confirm_toggle.html', {'task': task})
A GET request shows a confirmation page; a POST request actually flips
the flag, saves it, and redirects. Everything about this one piece of behavior lives in one place.
Redirecting After a Successful Action
Notice the view above ends a successful POST with redirect(), not
render(). This is the Post/Redirect/Get pattern, and it solves a real
problem: if a POST request were answered directly with a rendered page, refreshing that
page would ask the browser to resubmit the exact same POST — often re-running the
action a second time (imagine toggling completed back and forth just from hitting
refresh). Redirecting sends the browser to a fresh GET URL instead, so a refresh only
repeats a harmless GET.
# redirect() accepts a URL name, the same way reverse() does
return redirect('task-detail', task_id=task.id)
# it also accepts a model instance directly, if the model defines get_absolute_url()
return redirect(task)
# or a plain URL string
return redirect('/tasks/')
Reading Query Strings and Form Data
Incoming data reaches a view through two separate dictionary-like objects, and mixing them up is a common early mistake:
| Object | Holds | Typical use |
|---|---|---|
request.GET | Query string parameters, e.g. ?priority=high&completed=false | Filtering and searching, e.g. request.GET.get('priority') |
request.POST | Submitted form field data from a POST request | Reading values a user typed into a form, e.g. request.POST.get('title') |
def task_list(request):
tasks = Task.objects.all()
priority = request.GET.get('priority')
if priority:
tasks = tasks.filter(priority=priority)
return render(request, 'taskflow/task_list.html', {'tasks': tasks})
A visit to /tasks/?priority=high narrows the list before it ever reaches the template
— no form submission required, since the values are already sitting in the URL's query string.
Keeping Views Thin
None of the views above contain more than a handful of lines, and that's deliberate. As TaskFlow
grows, it's tempting to let a view balloon with business logic — pushing everything through
request.method checks and inline queries. A view function reads best when its job is
narrow: pull out what's needed, delegate the real decision-making to model methods or plain helper
functions, and hand a template exactly the context it needs to render.