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:

ObjectHoldsTypical use
request.GETQuery string parameters, e.g. ?priority=high&completed=falseFiltering and searching, e.g. request.GET.get('priority')
request.POSTSubmitted form field data from a POST requestReading 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.

Next up: Every function-based view here follows the same shape by hand. Class-Based Views shows how Django packages that shape into reusable classes for the common cases.