Skip to content

Commit f635997

Browse files
committed
Добавлен API CRUD для kanban board
1 parent f669cdc commit f635997

5 files changed

Lines changed: 170 additions & 0 deletions

File tree

kanban/serializers.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from django.db import transaction
2+
from rest_framework import serializers
3+
4+
from kanban.models import Board, BoardColumn
5+
from projects.models import Collaborator, Project
6+
7+
8+
class BoardSerializer(serializers.ModelSerializer):
9+
project = serializers.PrimaryKeyRelatedField(queryset=Project.objects.all())
10+
11+
class Meta:
12+
model = Board
13+
fields = (
14+
"id",
15+
"project",
16+
"name",
17+
"color",
18+
"icon",
19+
"description",
20+
"created_at",
21+
"updated_at",
22+
)
23+
read_only_fields = ("id", "created_at", "updated_at")
24+
25+
def validate_project(self, project: Project):
26+
request = self.context.get("request")
27+
user = getattr(request, "user", None)
28+
if not user or not user.is_authenticated:
29+
raise serializers.ValidationError("Требуется аутентификация")
30+
31+
if project.leader_id == user.id:
32+
return project
33+
34+
is_member = Collaborator.objects.filter(
35+
project_id=project.id,
36+
user_id=user.id,
37+
).exists()
38+
if not is_member:
39+
raise serializers.ValidationError("Пользователь не является участником проекта")
40+
return project
41+
42+
def create(self, validated_data):
43+
with transaction.atomic():
44+
board = super().create(validated_data)
45+
if not board.columns.exists():
46+
BoardColumn.objects.create(board=board, name="Бэклог", order=1)
47+
return board

kanban/tests/test_board_api.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from django.urls import reverse
2+
from rest_framework import status
3+
from rest_framework.test import APIClient
4+
5+
from kanban.models import Board
6+
from kanban.tests.base import BaseKanbanTestCase
7+
from projects.models import Project
8+
from users.models import CustomUser
9+
10+
11+
class BoardAPITests(BaseKanbanTestCase):
12+
def setUp(self):
13+
super().setUp()
14+
self.client = APIClient()
15+
self.client.force_authenticate(user=self.user)
16+
self.url = reverse("kanban:kanban-board-list")
17+
18+
def test_list_boards_returns_user_project_boards(self):
19+
response = self.client.get(self.url)
20+
self.assertEqual(response.status_code, status.HTTP_200_OK)
21+
self.assertGreaterEqual(len(response.data), 1)
22+
self.assertEqual(response.data[0]["project"], self.project.id)
23+
24+
def test_create_board_creates_backlog_column(self):
25+
payload = {
26+
"project": self.project.id,
27+
"name": "New Board",
28+
"color": "blue",
29+
"icon": "rocket",
30+
"description": "",
31+
}
32+
response = self.client.post(self.url, payload, format="json")
33+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
34+
board_id = response.data["id"]
35+
board = Board.objects.get(id=board_id)
36+
backlog = board.columns.first()
37+
self.assertIsNotNone(backlog)
38+
self.assertEqual(backlog.name, "Бэклог")
39+
40+
def test_cannot_create_board_for_foreign_project(self):
41+
foreign_leader = CustomUser.objects.create(
42+
email="foreign@example.com",
43+
first_name="Foreign",
44+
last_name="Leader",
45+
password="pass1234",
46+
birthday=self.user.birthday,
47+
)
48+
foreign_project = Project.objects.create(name="Foreign", leader=foreign_leader)
49+
payload = {
50+
"project": foreign_project.id,
51+
"name": "Forbidden",
52+
"color": "blue",
53+
"icon": "rocket",
54+
"description": "",
55+
}
56+
response = self.client.post(self.url, payload, format="json")
57+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

kanban/urls.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.urls import include, path
2+
from rest_framework.routers import DefaultRouter
3+
4+
from kanban.views import BoardViewSet
5+
6+
app_name = "kanban"
7+
8+
router = DefaultRouter()
9+
router.register(r"boards", BoardViewSet, basename="kanban-board")
10+
11+
urlpatterns = [path("", include(router.urls))]

kanban/views.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from django.db.models import Q
2+
from drf_yasg.utils import swagger_auto_schema
3+
from rest_framework import permissions, viewsets
4+
5+
from kanban.models import Board
6+
from kanban.serializers import BoardSerializer
7+
from projects.models import Collaborator
8+
9+
10+
class BoardViewSet(viewsets.ModelViewSet):
11+
serializer_class = BoardSerializer
12+
permission_classes = [permissions.IsAuthenticated]
13+
14+
def get_queryset(self):
15+
user = self.request.user
16+
collaborator_projects = Collaborator.objects.filter(user=user).values(
17+
"project_id"
18+
)
19+
return (
20+
Board.objects.select_related("project")
21+
.prefetch_related("columns")
22+
.filter(
23+
Q(project__leader_id=user.id) | Q(project_id__in=collaborator_projects)
24+
)
25+
.distinct()
26+
)
27+
28+
def perform_destroy(self, instance):
29+
# TODO: в будущем реализовать проверку прав на удаление досок
30+
return super().perform_destroy(instance)
31+
32+
@swagger_auto_schema(tags=["Kanban Boards"])
33+
def create(self, request, *args, **kwargs):
34+
return super().create(request, *args, **kwargs)
35+
36+
@swagger_auto_schema(tags=["Kanban Boards"])
37+
def list(self, request, *args, **kwargs):
38+
return super().list(request, *args, **kwargs)
39+
40+
@swagger_auto_schema(tags=["Kanban Boards"])
41+
def retrieve(self, request, *args, **kwargs):
42+
return super().retrieve(request, *args, **kwargs)
43+
44+
@swagger_auto_schema(tags=["Kanban Boards"])
45+
def partial_update(self, request, *args, **kwargs):
46+
return super().partial_update(request, *args, **kwargs)
47+
48+
@swagger_auto_schema(tags=["Kanban Boards"])
49+
def update(self, request, *args, **kwargs):
50+
return super().update(request, *args, **kwargs)
51+
52+
@swagger_auto_schema(tags=["Kanban Boards"])
53+
def destroy(self, request, *args, **kwargs):
54+
return super().destroy(request, *args, **kwargs)

procollab/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
path("programs/", include("partner_programs.urls", namespace="partner_programs")),
5151
path("rate-project/", include(("project_rates.urls", "rate_projects"))),
5252
path("feed/", include("feed.urls", namespace="feed")),
53+
path("kanban/", include("kanban.urls", namespace="kanban")),
5354
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
5455
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
5556
path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"),

0 commit comments

Comments
 (0)