17 Commits

Author SHA1 Message Date
Derinhelm
9152cc9772 Исправлены недочеты в оформлении 2021-04-28 15:47:08 +03:00
fdd40d4592 feat: Добавлено вычисления покрытия для тестов бекенда 2021-04-27 23:13:16 +03:00
0ee81d4b42 feat: Используем pytest для бекенда 2021-04-27 23:12:51 +03:00
Derinhelm
bba4be6b27 В README добавлена команда для запуска тестов 2021-04-27 20:38:49 +03:00
Derinhelm
4ab5f11dd2 Перегруппированы тесты 2021-04-27 17:52:16 +03:00
Derinhelm
e3b93f3524 Добавлены тесты для Item 2021-04-26 23:35:45 +03:00
Derinhelm
a6a31c8c10 Добавлены простейшие тесты на создание Item 2021-04-26 12:57:47 +03:00
Derinhelm
889acf6c45 Добавлены тесты для ToDoList 2021-04-26 11:46:01 +03:00
Derinhelm
7e02dff184 Добавлен тест на простейшее создание ToDoList
Для запуска docker-compose exec web python manage.py test
2021-04-25 21:58:27 +03:00
Derinhelm
61bb90540e Добавлены тесты 2021-04-25 21:58:27 +03:00
f43c652671 hotfix: Исправлено добавление списков и их элементов из API 2021-04-18 12:24:12 +03:00
Aleksey Lobanov
4886e2e9be Merge pull request #20 from AlekseyLobanov/feat_4.base_api
Добавлено базовое API + finished #4 #17
2021-04-17 15:01:03 +03:00
3f5083f8e4 feat: Реализовано базовое простое API
Минимальное, но самодостаточное
2021-04-17 15:00:04 +03:00
0f2f291c18 feat: Добавлена пагинация для API 2021-04-17 14:59:26 +03:00
8a26910651 db: Больше индексов для важных полей 2021-04-17 14:59:08 +03:00
64607efaf3 feat: Новая модель в админке и добавления поля finished 2021-04-17 14:58:49 +03:00
6352deda3c feat: Поддержка фильтрации на стороне Django 2021-04-17 14:57:38 +03:00
14 changed files with 373 additions and 26 deletions

1
.gitignore vendored
View File

@@ -65,6 +65,5 @@ target/
*.jsl
*.db
tmp*
test_*
.env*
venv*

View File

@@ -53,3 +53,8 @@ docker-compose up
```bash
docker-compose exec web python manage.py makemigrations backend
```
Для запуска тестов использовать
```bash
docker-compose run -e DJANGO_SETTINGS_MODULE=backend.settings web pytest --cov=backend
```

3
backend/.coveragerc Normal file
View File

@@ -0,0 +1,3 @@
[run]
omit =
backend/migrations/*

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from .models import ToDoList
from .models import ToDoList, ToDoItem
class ToDoListAdmin(admin.ModelAdmin):
@@ -9,4 +9,11 @@ class ToDoListAdmin(admin.ModelAdmin):
list_editable = ["title"]
class ToDoItemAdmin(admin.ModelAdmin):
model = ToDoItem
list_display = ["parent", "finished", "text", "created_at"]
list_editable = ["finished", "text"]
admin.site.register(ToDoList, ToDoListAdmin)
admin.site.register(ToDoItem, ToDoItemAdmin)

View File

@@ -1,20 +1,62 @@
from rest_framework import viewsets, serializers, permissions
from rest_framework import routers
from django_filters.rest_framework import DjangoFilterBackend
from .models import ToDoList
from .models import ToDoList, ToDoItem
class ToDoListField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context["request"].user
return ToDoList.objects.filter(user=user)
class ToDoItemSerializer(serializers.HyperlinkedModelSerializer):
parent = ToDoListField(many=False, read_only=False, help_text="ID родительского списка")
class Meta:
model = ToDoItem
fields = ["id", "text", "finished", "created_at", "parent"]
class ToDoItemViewSet(viewsets.ModelViewSet):
serializer_class = ToDoItemSerializer
permission_classes = [permissions.IsAuthenticated]
filter_backends = [DjangoFilterBackend]
filterset_fields = ["parent", "finished"]
def get_queryset(self):
user = self.request.user
if not user.is_authenticated:
# ветка только для генерации схемы
return ToDoItem.objects.all()
return ToDoItem.objects.filter(parent__user=user)
class ToDoListSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ToDoList
fields = ["title", "created_at"]
fields = ["id", "title", "created_at"]
def create(self, validated_data):
todo_list = ToDoList.objects.create(
user=self.context["request"].user, title=validated_data["title"]
)
return todo_list
class ToDoListViewSet(viewsets.ModelViewSet):
queryset = ToDoList.objects.all()
serializer_class = ToDoListSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
user = self.request.user
if not user.is_authenticated:
# ветка только для генерации схемы
return ToDoList.objects.all()
return ToDoList.objects.filter(user=user)
router = routers.DefaultRouter()
router.register(r"lists", ToDoListViewSet)
router.register(r"lists", ToDoListViewSet, basename="ToDoLists")
router.register(r"todo_items", ToDoItemViewSet, basename="ToDoItems")

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2 on 2021-04-17 11:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("backend", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="todoitem",
name="finished",
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.2 on 2021-04-17 11:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("backend", "0002_todoitem_finished"),
]
operations = [
migrations.AlterField(
model_name="todoitem",
name="created_at",
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
migrations.AlterField(
model_name="todoitem",
name="finished",
field=models.BooleanField(db_index=True, default=False),
),
migrations.AlterField(
model_name="todolist",
name="created_at",
field=models.DateTimeField(auto_now_add=True, db_index=True),
),
]

View File

@@ -5,10 +5,11 @@ from django.contrib.auth.models import User
class ToDoList(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, default=None)
title = models.CharField(max_length=250)
created_at = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
class ToDoItem(models.Model):
parent = models.ForeignKey(ToDoList, on_delete=models.CASCADE, null=False, default=None)
text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
finished = models.BooleanField(default=False, null=False, db_index=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)

View File

@@ -27,8 +27,8 @@ SECRET_KEY = "django-insecure-toz+*lt(ejm!l*)92w2ciqoh^1kz#a(abbpcn54-dbw(nxoy&7
DEBUG = True
ALLOWED_HOSTS = []
if DEBUG:
ALLOWED_HOSTS = ["0.0.0.0"]
if DEBUG:
ALLOWED_HOSTS = ["0.0.0.0", "localhost", "127.0.0.1", "ALLOWED_HOSTS", "testserver"]
# Application definition
@@ -45,13 +45,7 @@ INSTALLED_APPS = [
]
SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': {
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
}
}
"SECURITY_DEFINITIONS": {"Bearer": {"type": "apiKey", "name": "Authorization", "in": "header"}}
}
MIDDLEWARE = [
@@ -130,7 +124,12 @@ SIMPLE_JWT = {
}
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication",)
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 100,
}

View File

@@ -13,13 +13,13 @@ from drf_yasg import openapi
from .api import router
schema_view = get_schema_view(
openapi.Info(
title="ToDo List",
default_version='v1',
description="Swagger Interface for ToDo List",
),
public=True,
permission_classes=(permissions.AllowAny,),
openapi.Info(
title="ToDo List",
default_version="v1",
description="Swagger Interface for ToDo List",
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
@@ -28,5 +28,5 @@ urlpatterns = [
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("api/", include(router.urls)),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path("swagger/", schema_view.with_ui("swagger", cache_timeout=0), name="schema-swagger-ui"),
]

View File

@@ -1,4 +1,7 @@
pytest==6.2.3
pytest-cov==2.11.1
djangorestframework==3.12.4
django-filter==2.4.0
markdown==3.3.4
appdirs==1.4.4
asgiref==3.3.4

View File

@@ -0,0 +1,3 @@
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

154
backend/tests/test_item.py Normal file
View File

@@ -0,0 +1,154 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User
from .test_todo import create_todo
class ItemTest(APITestCase):
"""Tests API for items."""
def prepare(self):
user = User.objects.create_user("test_user4", "test@test.com", "test_password")
self.client.force_authenticate(user=user)
to_do_id_1 = create_todo(self.client, "ToDoList1").data["id"]
to_do_id_2 = create_todo(self.client, "ToDoList2").data["id"]
return to_do_id_1, to_do_id_2
def get(self, expected_titles, todo_id=None, finished=None):
url = reverse("ToDoItems-list")
data = {}
if finished is not None:
data["finished"] = finished
if todo_id is not None:
data["parent"] = todo_id
response = self.client.get(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
real_titles = [(d["text"], d["parent"]) for d in response.data["results"]]
self.assertEqual(real_titles, expected_titles)
if finished is not None:
item_status = [data["finished"] for data in response.data["results"]]
self.assertEqual(finished, all(item_status))
def post(self, item_text, todo_id, finished=None):
url = reverse("ToDoItems-list")
if finished is not None:
data = {"text": item_text, "parent": todo_id, "finished": finished}
else:
data = {"text": item_text, "parent": todo_id}
response = self.client.post(url, data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
check_finished = False if (finished is None) else finished
self.assertEqual(response.data["text"], item_text)
self.assertEqual(response.data["parent"], todo_id)
self.assertEqual(response.data["finished"], check_finished)
return response.data["id"], response.data["finished"]
def get_by_id(self, id, text, finished, parent):
url_with_id = reverse("ToDoItems-detail", args=(id,))
response = self.client.get(url_with_id, {id: id}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["text"], text)
self.assertEqual(response.data["finished"], finished)
self.assertEqual(response.data["parent"], parent)
def put(self, id, text, parent, finished=None):
url_with_id = reverse("ToDoItems-detail", args=(id,))
data = {"text": text, "parent": parent}
if finished is not None:
data["finished"] = finished
response = self.client.put(url_with_id, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["text"], text)
self.assertEqual(response.data["parent"], parent)
if finished is not None:
self.assertEqual(response.data["finished"], finished)
def patch(self, id, text=None, finished=None, parent=None):
url_with_id = reverse("ToDoItems-detail", args=(id,))
data = {}
if text is not None:
data["text"] = text
if finished is not None:
data["finished"] = finished
if parent is not None:
data["parent"] = parent
response = self.client.patch(url_with_id, data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
if text is not None:
self.assertEqual(response.data["text"], text)
if finished is not None:
self.assertEqual(response.data["finished"], finished)
if parent is not None:
self.assertEqual(response.data["parent"], parent)
def delete(self, id, title, finished, to_do_id):
self.get_by_id(id, title, finished, to_do_id)
url_with_id = reverse("ToDoItems-detail", args=(id,))
response = self.client.delete(url_with_id, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def test_create_delete(self):
"""
/todo_items/: get, post (create)
/todo_items/{id}/: get (read), delete
"""
to_do_id_1, to_do_id_2 = self.prepare()
self.get([], to_do_id_1)
item_text_1, item_text_2, item_text_3, item_text_4 = "Item1", "Item2", "Item3", "Item4"
item_id_1, item_finished_1 = self.post(item_text_1, to_do_id_1)
self.get([(item_text_1, to_do_id_1)], to_do_id_1)
item_id_2, item_finished_2 = self.post(item_text_2, to_do_id_1, finished=False)
self.get([(item_text_1, to_do_id_1), (item_text_2, to_do_id_1)], to_do_id_1)
item_id_3, item_finished_3 = self.post(item_text_3, to_do_id_1, finished=True)
self.get(
[(item_text_1, to_do_id_1), (item_text_2, to_do_id_1), (item_text_3, to_do_id_1)],
to_do_id_1,
)
item_id_4, item_finished_4 = self.post(item_text_4, to_do_id_2, finished=False)
self.get(
[
(item_text_1, to_do_id_1),
(item_text_2, to_do_id_1),
(item_text_3, to_do_id_1),
(item_text_4, to_do_id_2),
]
)
self.get(
[(item_text_1, to_do_id_1), (item_text_2, to_do_id_1), (item_text_3, to_do_id_1)],
to_do_id_1,
)
self.get([(item_text_1, to_do_id_1), (item_text_2, to_do_id_1)], to_do_id_1, finished=False)
self.get([(item_text_3, to_do_id_1)], to_do_id_1, finished=True)
self.get_by_id(item_id_1, item_text_1, item_finished_1, to_do_id_1)
self.get_by_id(item_id_2, item_text_2, item_finished_2, to_do_id_1)
self.get_by_id(item_id_3, item_text_3, item_finished_3, to_do_id_1)
self.delete(item_id_3, item_text_3, item_finished_3, to_do_id_1)
self.get([(item_text_1, to_do_id_1), (item_text_2, to_do_id_1)], to_do_id_1)
def test_update(self):
"""
/todo_items/{id}/: put (update), patch (partial_update)
"""
to_do_id_1, to_do_id_2 = self.prepare()
item_text_1 = "Item1"
item_id_1, item_finished_1 = self.post(item_text_1, to_do_id_1)
item_text_1_2 = "Item5"
self.put(item_id_1, item_text_1_2, to_do_id_2)
self.put(item_id_1, item_text_1_2, to_do_id_2, finished=False)
self.put(item_id_1, item_text_1_2, to_do_id_2, finished=True)
item_text_1_3 = "Item6"
self.patch(item_id_1, parent=to_do_id_1)
self.patch(item_id_1, finished=True)
self.patch(item_id_1, text=item_text_1_3)

View File

@@ -0,0 +1,85 @@
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth.models import User
def create_todo(client, title):
url = reverse("ToDoLists-list")
response = client.post(url, {"title": title}, format="json")
return response
class ToDoTest(APITestCase):
"""Tests API for todo."""
def get(self, expected_titles):
url = reverse("ToDoLists-list")
response = self.client.get(url, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
real_titles = [data["title"] for data in response.data["results"]]
self.assertEqual(real_titles, expected_titles)
def post(self, to_do_title):
response = create_todo(self.client, to_do_title)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["title"], to_do_title)
return response.data["id"]
def get_by_id(self, id, expected_title):
url_with_id = reverse("ToDoLists-detail", args=(id,))
response = self.client.get(url_with_id, {id: id}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["title"], expected_title)
def put(self, id, new_title):
url_with_id = reverse("ToDoLists-detail", args=(id,))
response = self.client.put(url_with_id, {"title": new_title}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["title"], new_title)
def patch(self, id, new_title):
url_with_id = reverse("ToDoLists-detail", args=(id,))
response = self.client.patch(url_with_id, {"title": new_title}, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["title"], new_title)
def delete(self, id, title):
self.get_by_id(id, title)
url_with_id = reverse("ToDoLists-detail", args=(id,))
response = self.client.delete(url_with_id, {}, format="json")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
def prepare(self):
user = User.objects.create_user("test_user", "test@test.com", "test_password")
self.client.force_authenticate(user=user)
def test_create_delete(self):
"""
lists/{id}/: put (update), patch (partial_update)
"""
self.prepare()
to_do_title_1 = "ToDoList1"
to_do_id1 = self.post(to_do_title_1)
self.put(to_do_id1, "ToDoList11")
self.patch(to_do_id1, "ToDoList12")
def test_todo(self):
"""
lists/: get, post
lists/{id}/: get, delete
"""
self.prepare()
self.get([])
to_do_title_1, to_do_title_2 = "ToDoList1", "ToDoList2"
to_do_id1 = self.post(to_do_title_1)
self.get([to_do_title_1])
to_do_id2 = self.post(to_do_title_2)
self.get([to_do_title_1, to_do_title_2])
self.get_by_id(to_do_id1, to_do_title_1)
self.get_by_id(to_do_id2, to_do_title_2)
self.delete(to_do_id2, to_do_title_2)
self.get([to_do_title_1])