Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
[Tasks API] Implement base API views (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
shorodilov committed Feb 12, 2024
2 parents c352990 + b3373b0 commit b9e3dc0
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 10 deletions.
13 changes: 12 additions & 1 deletion assets/js/main.bundle.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions tasks/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
Tasks application API resources
"""

import uuid

from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.request import Request
from rest_framework.response import Response

from tasks.models import TaskModel
from tasks.serializers import TaskModelReadSerializer, TaskModelWriteSerializer


@api_view(["GET", "POST"])
def tasks_list(request: Request) -> Response:
"""
Handle requests to task list endpoint
"""

if request.method == "GET":
qs = TaskModel.objects.all()
serializer = TaskModelReadSerializer(qs, many=True)

return Response(serializer.data)

serializer = TaskModelWriteSerializer(data=request.data)
if serializer.is_valid():
serializer.save()

return Response(serializer.data, status=status.HTTP_201_CREATED)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


@api_view(["GET", "PUT", "PATCH", "DELETE"])
def tasks_detail(request: Request, pk: uuid.UUID) -> Response:
"""
Handle requests to task detail endpoint
"""

try:
instance = TaskModel.objects.get(pk=pk)
except TaskModel.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)

if request.method == "GET":
serializer = TaskModelReadSerializer(instance)

return Response(serializer.data) # HTTP_200_OK

if request.method == "DELETE":
instance.delete()

return Response(status=status.HTTP_204_NO_CONTENT)

if request.method == "PATCH":
serializer = TaskModelWriteSerializer(instance, request.data,
partial=True)
else: # PUT
serializer = TaskModelWriteSerializer(instance, request.data)

if serializer.is_valid():
serializer.save()

return Response(serializer.data, status=status.HTTP_202_ACCEPTED)

return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
14 changes: 14 additions & 0 deletions tasks/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
Tasks application API routes
"""

from django.urls import path

from tasks import resources

app_name = "tasks"
urlpatterns = [
path("tasks/", resources.tasks_list, name="tasks-list"),
path("tasks/<uuid:pk>/", resources.tasks_detail, name="tasks-detail")
]
8 changes: 4 additions & 4 deletions tasks/templates/tasks/_actions.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{% if object.completed %}
<button class="btn btn-outline-warning mx-3 {{ update_permission }}"
hx-patch="{# TODO: GH-73 #}" hx-swap="none"
hx-vals="js:{completed:true}">
hx-patch="{% url "api:tasks-detail" object.pk %}" hx-swap="none"
hx-vals="js:{completed:false}" hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}">
Reopen
</button>
{% else %}
<button class="btn btn-outline-success mx-3 {{ update_permission }}"
hx-patch="{# TODO: GH-73 #}" hx-swap="none"
hx-vals="js:{completed:true}">
hx-patch="{% url "api:tasks-detail" object.pk %}" hx-swap="none"
hx-vals="js:{completed:true}" hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}">
Complete
</button>
{% endif %}
Expand Down
11 changes: 6 additions & 5 deletions tasks/templates/tasks/_task_tr.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@
<div class="d-flex flex-row justify-content-between align-items-center">
{% if object.completed %}
<i class="bi bi-arrow-repeat {{ update_permission }}" role="button"
hx-patch="{# TODO: GH-73 #}" hx-swap="none"
hx-vals="js:{completed:false}" hx-headers="js:{}"></i>
hx-patch="{% url "api:tasks-detail" object.pk %}" hx-swap="none"
hx-vals="js:{completed:false}" hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}"></i>
{% else %}
<i class="bi bi-check-lg {{ update_permission }}" role="button"
hx-patch="{# TODO: GH-73 #}" hx-swap="none"
hx-vals="js:{completed:true}" hx-headers="js:{}"></i>
hx-patch="{% url "api:tasks-detail" object.pk %}" hx-swap="none"
hx-vals="js:{completed:true}" hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}"></i>
{% endif %}
<i class="bi bi-trash {{ delete_permission }}" role="button"
hx-delete="{# TODO: GH-74 #}" hx-target="closest tr" hx-swap="outerHTML"></i>
hx-delete="{% url "api:tasks-detail" object.pk %}" hx-target="closest tr" hx-swap="outerHTML"
hx-headers="js:{'X-CSRFToken': getCookieValue('csrftoken')}"></i>
</div>
</td>
</tr>
3 changes: 3 additions & 0 deletions tasks/templates/tasks/task_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<div class="card shadow" id="taskDetailContainer">
<div class="card-header">
<h1 class="card-title h3 text-center" id="summary">
{% if object.completed %}
<i class="bi bi-check-lg text-success"></i>
{% endif %}
<span>{{ object.summary }}</span>
</h1>
<hr class="border">
Expand Down
63 changes: 63 additions & 0 deletions tasks/tests/integration/test_detail_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from http import HTTPStatus

from django.contrib.auth import get_user_model
from rest_framework import test
from rest_framework.reverse import reverse_lazy

from tasks.models import TaskModel

UserModel = get_user_model()


class TestTasksListAPI(test.APITestCase):
fixtures = ["users"]
reporter: UserModel = None
assignee: UserModel = None

@classmethod
def setUpTestData(cls) -> None:
cls.reporter = UserModel.objects.get(pk=2)
cls.assignee = UserModel.objects.get(pk=3)
cls.data = {
"summary": "Test tasks list API",
"reporter": cls.reporter.pk,
}

def setUp(self) -> None:
self.client = test.APIClient()
self.instance = TaskModel.objects.create(
summary="Existing task",
reporter=self.reporter,
)
self.url_path = reverse_lazy(
"api:tasks-detail",
args=(self.instance.pk,)
)
self.url_404 = reverse_lazy(
"api:tasks-detail",
args=("dc9dcb0a-e20b-404e-a4e6-9627f7bc118d",)
)

def test_get(self):
response = self.client.get(self.url_path)
self.assertEqual(response.status_code, HTTPStatus.OK)

def test_put(self):
response = self.client.put(self.url_path, self.data)
self.assertEqual(response.status_code, HTTPStatus.ACCEPTED)

def test_patch(self):
response = self.client.patch(self.url_path, {"completed": False})
self.assertEqual(response.status_code, HTTPStatus.ACCEPTED)

def test_delete(self):
response = self.client.delete(self.url_path)
self.assertEqual(response.status_code, HTTPStatus.NO_CONTENT)

def test_not_found(self):
response = self.client.get(self.url_404)
self.assertEqual(response.status_code, HTTPStatus.NOT_FOUND)

def test_bad_request(self):
response = self.client.put(self.url_path, {})
self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
43 changes: 43 additions & 0 deletions tasks/tests/integration/test_list_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from http import HTTPStatus

from django.contrib.auth import get_user_model
from rest_framework import test
from rest_framework.reverse import reverse_lazy

from tasks.models import TaskModel

UserModel = get_user_model()


class TestTasksListAPI(test.APITestCase):
fixtures = ["users", "tasks"]
reporter: UserModel = None

@classmethod
def setUpTestData(cls) -> None:
cls.url_path = reverse_lazy("api:tasks-list")
cls.reporter = UserModel.objects.get(pk=2)
cls.data = {
"summary": "Test tasks list API",
"reporter": cls.reporter.pk,
}

def setUp(self) -> None:
self.client = test.APIClient()

def test_get(self):
response = self.client.get(self.url_path)

self.assertEqual(response.status_code, HTTPStatus.OK)

def test_post(self):
response = self.client.post(self.url_path, self.data)

qs = TaskModel.objects.filter(summary=self.data["summary"])
self.assertEqual(response.status_code, HTTPStatus.CREATED)
self.assertTrue(qs.exists())

def test_bad_request(self):
response = self.client.post(self.url_path, {})

self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
1 change: 1 addition & 0 deletions tasktracker/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("tasks.routes", namespace="api")),
path("", include("users.urls", namespace="users")),
path("", include("tasks.urls", namespace="tasks")),
]
Expand Down

0 comments on commit b9e3dc0

Please sign in to comment.