Menu
CoddyTech

Project: Clear Done Tasks

Part of the JSON APIs section of Coddy's Django journey. Lesson 16 of 19.

Done tasks pile up, so the tracker needs a way to clear them in one go. Clearing works on many tasks at once, not on one task, so it gets its own URL, and since it changes data it accepts only POST: browsers, link previews and search engines send GETs without asking anyone, so a GET must never delete.

One detail is new. A QuerySet's delete() returns a pair, the number of rows deleted and a dictionary of counts per model:

>>> Book.objects.filter(year__lt=1900).delete()
(2, {'books.Book': 2})

The rest you have built before: a view, its route, two decorators and a JSON response.

challenge icon

Challenge

Hard

Add an endpoint that clears every done task.

  1. In tasks/views.py, add a view clear_done. It deletes every done task and responds with {"deleted": 1}, the number deleted. Any method but POST gets 405, and it works with no CSRF token.
  2. In tasks/urls.py, add the route "api/tasks/clear-done/" named clear_done.

Run the line below in the Shell tab: it prints {'deleted': 1}, because Call the bank was the only done task.

from django.test import Client; print(Client().post("/api/tasks/clear-done/").json())

Try it yourself

import json

from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET, require_http_methods

from .models import Task


def task_to_dict(task):
    return {
        "id": task.id,
        "title": task.title,
        "done": task.done,
        "priority": task.priority,
    }


def home(request):
    return render(request, "tasks/home.html")


def task_page(request):
    return render(request, "tasks/list.html", {"tasks": Task.objects.all()})


def task_row(request, pk):
    task = get_object_or_404(Task, pk=pk)
    return render(request, "tasks/list.html#task-row", {"task": task})


@require_GET
def stats(request):
    total = Task.objects.count()
    done = Task.objects.filter(done=True).count()
    data = {
        "total": total,
        "done": done,
        "open": total - done,
    }
    return JsonResponse(data)


@csrf_exempt
@require_http_methods(["GET", "POST"])
def task_list(request):
    if request.method == "POST":
        try:
            data = json.loads(request.body)
        except json.JSONDecodeError:
            return JsonResponse({"error": "The body is not valid JSON."}, status=400)
        errors = {}
        title = data.get("title", "").strip()
        if not title:
            errors["title"] = "This field is required."
        priority = data.get("priority", "normal")
        if priority not in Task.Priority.values:
            errors["priority"] = "Choose low, normal or high."
        if errors:
            return JsonResponse({"errors": errors}, status=400)
        task = Task.objects.create(
            title=title,
            priority=priority,
        )
        return JsonResponse(task_to_dict(task), status=201)
    tasks = Task.objects.all()
    done = request.GET.get("done")
    if done == "true":
        tasks = tasks.filter(done=True)
    elif done == "false":
        tasks = tasks.filter(done=False)
    priority = request.GET.get("priority")
    if priority:
        tasks = tasks.filter(priority=priority)
    return JsonResponse([task_to_dict(task) for task in tasks], safe=False)


def not_found():
    return JsonResponse({"error": "Task not found"}, status=404)


@method_decorator(csrf_exempt, name="dispatch")
class TaskDetail(View):
    def get(self, request, pk):
        task = Task.objects.filter(pk=pk).first()
        if task is None:
            return not_found()
        return JsonResponse(task_to_dict(task))

    def patch(self, request, pk):
        task = Task.objects.filter(pk=pk).first()
        if task is None:
            return not_found()
        data = json.loads(request.body)
        if "title" in data:
            task.title = data["title"]
        if "done" in data:
            task.done = data["done"]
        task.save()
        return JsonResponse(task_to_dict(task))

    def delete(self, request, pk):
        task = Task.objects.filter(pk=pk).first()
        if task is None:
            return not_found()
        task.delete()
        return HttpResponse(status=204)

All lessons in JSON APIs

Practice on your own: Django playground