From b39c080de3ca41cc9639e47a20e3bc3d4b5d6af6 Mon Sep 17 00:00:00 2001 From: xoliqberdiyev Date: Wed, 29 Apr 2026 18:40:51 +0500 Subject: [PATCH 1/2] add tasks app --- config/conf/apps.py | 7 +- config/conf/navigation.py | 26 +++++++ core/apps/accounts/serializers/user.py | 19 ++++- core/apps/tasks/__init__.py | 0 core/apps/tasks/admin/__init__.py | 4 + core/apps/tasks/admin/column.py | 7 ++ core/apps/tasks/admin/comment.py | 7 ++ core/apps/tasks/admin/label.py | 7 ++ core/apps/tasks/admin/task.py | 7 ++ core/apps/tasks/apps.py | 8 ++ core/apps/tasks/choices/comment.py | 6 ++ core/apps/tasks/choices/task.py | 7 ++ core/apps/tasks/migrations/0001_initial.py | 77 +++++++++++++++++++ .../0002_alter_comment_created_by.py | 21 +++++ core/apps/tasks/migrations/__init__.py | 0 core/apps/tasks/models/__init__.py | 4 + core/apps/tasks/models/column.py | 10 +++ core/apps/tasks/models/comment.py | 16 ++++ core/apps/tasks/models/label.py | 10 +++ core/apps/tasks/models/task.py | 20 +++++ core/apps/tasks/serializers/column.py | 11 +++ core/apps/tasks/serializers/comment.py | 43 +++++++++++ core/apps/tasks/serializers/label.py | 11 +++ core/apps/tasks/serializers/task.py | 30 ++++++++ core/apps/tasks/urls.py | 7 ++ core/apps/tasks/views/column.py | 0 core/apps/tasks/views/comment.py | 0 core/apps/tasks/views/task.py | 0 28 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 core/apps/tasks/__init__.py create mode 100644 core/apps/tasks/admin/__init__.py create mode 100644 core/apps/tasks/admin/column.py create mode 100644 core/apps/tasks/admin/comment.py create mode 100644 core/apps/tasks/admin/label.py create mode 100644 core/apps/tasks/admin/task.py create mode 100644 core/apps/tasks/apps.py create mode 100644 core/apps/tasks/choices/comment.py create mode 100644 core/apps/tasks/choices/task.py create mode 100644 core/apps/tasks/migrations/0001_initial.py create mode 100644 core/apps/tasks/migrations/0002_alter_comment_created_by.py create mode 100644 core/apps/tasks/migrations/__init__.py create mode 100644 core/apps/tasks/models/__init__.py create mode 100644 core/apps/tasks/models/column.py create mode 100644 core/apps/tasks/models/comment.py create mode 100644 core/apps/tasks/models/label.py create mode 100644 core/apps/tasks/models/task.py create mode 100644 core/apps/tasks/serializers/column.py create mode 100644 core/apps/tasks/serializers/comment.py create mode 100644 core/apps/tasks/serializers/label.py create mode 100644 core/apps/tasks/serializers/task.py create mode 100644 core/apps/tasks/urls.py create mode 100644 core/apps/tasks/views/column.py create mode 100644 core/apps/tasks/views/comment.py create mode 100644 core/apps/tasks/views/task.py diff --git a/config/conf/apps.py b/config/conf/apps.py index d748133..39cda63 100644 --- a/config/conf/apps.py +++ b/config/conf/apps.py @@ -1,11 +1,11 @@ from config.env import env APPS = [ - + "cacheops", "rosetta", "django_ckeditor_5", - + "drf_spectacular", "rest_framework", "corsheaders", @@ -14,9 +14,10 @@ APPS = [ "rest_framework_simplejwt", "django_core", "core.apps.accounts.apps.AccountsConfig", + 'core.apps.tasks.apps.TasksConfig', ] if env.bool("SILK_ENABLED", False): APPS += [ - + ] diff --git a/config/conf/navigation.py b/config/conf/navigation.py index a011d9b..b92647b 100644 --- a/config/conf/navigation.py +++ b/config/conf/navigation.py @@ -212,5 +212,31 @@ PAGES = [ "link": reverse_lazy("admin:accounts_role_changelist"), }, ] + }, + { + "title": _("Task Management"), + "separator": True, + "items": [ + { + "title": _("Task"), + "icon": "task", + "link": reverse_lazy("admin:tasks_task_changelist"), + }, + { + "title": _("Column"), + "icon": "tag", + "link": reverse_lazy("admin:tasks_column_changelist"), + }, + { + "title": _("Comment"), + "icon": "message", + "link": reverse_lazy("admin:tasks_comment_changelist"), + }, + { + "title": _("Label"), + "icon": "tag", + "link": reverse_lazy("admin:tasks_label_changelist"), + }, + ] } ] diff --git a/core/apps/accounts/serializers/user.py b/core/apps/accounts/serializers/user.py index 9019962..b93910c 100644 --- a/core/apps/accounts/serializers/user.py +++ b/core/apps/accounts/serializers/user.py @@ -54,4 +54,21 @@ class UserCreateSerializer(serializers.ModelSerializer): "first_name", "last_name", "password", - "role"] \ No newline at end of file + "role" + ] + + +class ShortUserSerializer(serializers.ModelSerializer): + class Meta: + model = get_user_model() + fields = [ + 'id', + 'full_name', + 'avatar', + ] + + def get_avatar(self, obj): + request = self.context.get('request') + if obj.avatar: + return request.build_absolute_uri(obj.avatar.url) + return None \ No newline at end of file diff --git a/core/apps/tasks/__init__.py b/core/apps/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/tasks/admin/__init__.py b/core/apps/tasks/admin/__init__.py new file mode 100644 index 0000000..f0884af --- /dev/null +++ b/core/apps/tasks/admin/__init__.py @@ -0,0 +1,4 @@ +from .column import * +from .comment import * +from .task import * +from .label import * diff --git a/core/apps/tasks/admin/column.py b/core/apps/tasks/admin/column.py new file mode 100644 index 0000000..60363ca --- /dev/null +++ b/core/apps/tasks/admin/column.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from core.apps.tasks.models import Column + +@admin.register(Column) +class ColumnAdmin(admin.ModelAdmin): + list_display = ('name',) diff --git a/core/apps/tasks/admin/comment.py b/core/apps/tasks/admin/comment.py new file mode 100644 index 0000000..2e7e2e3 --- /dev/null +++ b/core/apps/tasks/admin/comment.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from core.apps.tasks.models import Comment + +@admin.register(Comment) +class CommentAdmin(admin.ModelAdmin): + list_display = ('created_by', 'type') diff --git a/core/apps/tasks/admin/label.py b/core/apps/tasks/admin/label.py new file mode 100644 index 0000000..66eaf05 --- /dev/null +++ b/core/apps/tasks/admin/label.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from core.apps.tasks.models import Label + +@admin.register(Label) +class LabelAdmin(admin.ModelAdmin): + list_display = ('name',) diff --git a/core/apps/tasks/admin/task.py b/core/apps/tasks/admin/task.py new file mode 100644 index 0000000..b5a1afe --- /dev/null +++ b/core/apps/tasks/admin/task.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from core.apps.tasks.models import Task + +@admin.register(Task) +class TaskAdmin(admin.ModelAdmin): + list_display = ('name', 'created_by', 'priority') diff --git a/core/apps/tasks/apps.py b/core/apps/tasks/apps.py new file mode 100644 index 0000000..410dd85 --- /dev/null +++ b/core/apps/tasks/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class TasksConfig(AppConfig): + name = "core.apps.tasks" + + def ready(self): + from core.apps.tasks import admin diff --git a/core/apps/tasks/choices/comment.py b/core/apps/tasks/choices/comment.py new file mode 100644 index 0000000..0426ef1 --- /dev/null +++ b/core/apps/tasks/choices/comment.py @@ -0,0 +1,6 @@ +from django.db import models + + +class MessageChoice(models.TextChoices): + FILE = "file", "File" + TEXT = "text", "Text" diff --git a/core/apps/tasks/choices/task.py b/core/apps/tasks/choices/task.py new file mode 100644 index 0000000..e6c8a71 --- /dev/null +++ b/core/apps/tasks/choices/task.py @@ -0,0 +1,7 @@ +from django.db import models + + +class PriorityChoice(models.TextChoices): + LOW = "low", "Low" + MEDIUM = "medium", "Medium" + HIGH = "high", "High" diff --git a/core/apps/tasks/migrations/0001_initial.py b/core/apps/tasks/migrations/0001_initial.py new file mode 100644 index 0000000..80fb206 --- /dev/null +++ b/core/apps/tasks/migrations/0001_initial.py @@ -0,0 +1,77 @@ +# Generated by Django 5.2.7 on 2026-04-29 13:18 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Column', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Label', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=255)), + ('description', models.TextField(blank=True)), + ('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], max_length=255)), + ('from_date', models.DateField(blank=True, null=True)), + ('to_date', models.DateField(blank=True, null=True)), + ('assignees', models.ManyToManyField(related_name='assigned_tasks', to=settings.AUTH_USER_MODEL)), + ('column', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='tasks.column')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_tasks', to=settings.AUTH_USER_MODEL)), + ('labels', models.ManyToManyField(related_name='tasks', to='tasks.label')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('message', models.TextField()), + ('file', models.FileField(blank=True, null=True, upload_to='comments/')), + ('type', models.CharField(choices=[('file', 'File'), ('text', 'Text')], max_length=255)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_comments', to=settings.AUTH_USER_MODEL)), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tasks.task')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/core/apps/tasks/migrations/0002_alter_comment_created_by.py b/core/apps/tasks/migrations/0002_alter_comment_created_by.py new file mode 100644 index 0000000..cf25212 --- /dev/null +++ b/core/apps/tasks/migrations/0002_alter_comment_created_by.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.7 on 2026-04-29 13:20 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='comment', + name='created_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/apps/tasks/migrations/__init__.py b/core/apps/tasks/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/tasks/models/__init__.py b/core/apps/tasks/models/__init__.py new file mode 100644 index 0000000..f0884af --- /dev/null +++ b/core/apps/tasks/models/__init__.py @@ -0,0 +1,4 @@ +from .column import * +from .comment import * +from .task import * +from .label import * diff --git a/core/apps/tasks/models/column.py b/core/apps/tasks/models/column.py new file mode 100644 index 0000000..22f8f0a --- /dev/null +++ b/core/apps/tasks/models/column.py @@ -0,0 +1,10 @@ +from django.db import models + +from django_core.models import AbstractBaseModel + + +class Column(AbstractBaseModel): + name = models.CharField(max_length=255) + + def __str__(self): + return self.name diff --git a/core/apps/tasks/models/comment.py b/core/apps/tasks/models/comment.py new file mode 100644 index 0000000..71d6925 --- /dev/null +++ b/core/apps/tasks/models/comment.py @@ -0,0 +1,16 @@ +from django.db import models + +from django_core.models import AbstractBaseModel + +from core.apps.tasks.choices.comment import MessageChoice + + +class Comment(AbstractBaseModel): + task = models.ForeignKey('tasks.Task', on_delete=models.CASCADE, related_name='comments') + message = models.TextField() + file = models.FileField(upload_to='comments/', blank=True, null=True) + type = models.CharField(max_length=255, choices=MessageChoice.choices) + created_by = models.ForeignKey('accounts.User', on_delete=models.CASCADE, related_name='comments') + + def __str__(self): + return f"{self.content} created by {self.created_by}" diff --git a/core/apps/tasks/models/label.py b/core/apps/tasks/models/label.py new file mode 100644 index 0000000..ae29ab4 --- /dev/null +++ b/core/apps/tasks/models/label.py @@ -0,0 +1,10 @@ +from django.db import models + +from django_core.models import AbstractBaseModel + + +class Label(AbstractBaseModel): + name = models.CharField(max_length=255) + + def __str__(self): + return self.name diff --git a/core/apps/tasks/models/task.py b/core/apps/tasks/models/task.py new file mode 100644 index 0000000..61f33a5 --- /dev/null +++ b/core/apps/tasks/models/task.py @@ -0,0 +1,20 @@ +from django.db import models + +from django_core.models import AbstractBaseModel + +from core.apps.tasks.choices.task import PriorityChoice + + +class Task(AbstractBaseModel): + column = models.ForeignKey('tasks.Column', on_delete=models.CASCADE, related_name='tasks') + name = models.CharField(max_length=255) + description = models.TextField(blank=True) + priority = models.CharField(max_length=255, choices=PriorityChoice.choices) + from_date = models.DateField(null=True, blank=True) + to_date = models.DateField(null=True, blank=True) + labels = models.ManyToManyField('tasks.Label', related_name='tasks') + assignees = models.ManyToManyField('accounts.User', related_name='assigned_tasks') + created_by = models.ForeignKey('accounts.User', on_delete=models.CASCADE, related_name='created_tasks') + + def __str__(self): + return f"{self.name} created by {self.created_by}" diff --git a/core/apps/tasks/serializers/column.py b/core/apps/tasks/serializers/column.py new file mode 100644 index 0000000..fd49f94 --- /dev/null +++ b/core/apps/tasks/serializers/column.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from core.apps.tasks.models.column import Column + + +class ColumnSerializer(serializers.ModelSerializer): + class Meta: + model = Column + fields = [ + 'id', 'name' + ] diff --git a/core/apps/tasks/serializers/comment.py b/core/apps/tasks/serializers/comment.py new file mode 100644 index 0000000..5500fa0 --- /dev/null +++ b/core/apps/tasks/serializers/comment.py @@ -0,0 +1,43 @@ +from django.db import transaction + +from rest_framework import serializers + +from core.apps.tasks.models.comment import Comment +from core.apps.tasks.models.task import Task + + +class CommentSerializer(serializers.ModelSerializer): + class Meta: + model = Comment + fields = [ + 'id', 'message', 'file', 'type', 'created_by' + ] + + def get_created_by(self, obj): + request = self.context.get('request') + return { + "id": obj.created_by.id, + "full_name": obj.created_by.full_name, + "avatar": request.build_absolute_uri(obj.created_by.avatar.url) if obj.created_by.avatar else None + } + + +class CommentCreateSerializer(serializers.ModelSerializer): + class Meta: + model = Comment + fields = [ + 'id', 'message', 'file', 'type', 'task' + ] + + def validate(self, data): + task = Task.objects.filter(id=data['task']).first() + if not task: + raise serializers.ValidationError("Task not found") + data['task'] = task + return data + + def create(self, validated_data): + with transaction.atomic(): + task = validated_data.pop('task') + comment = Comment.objects.create(task=task, created_by=self.context['request'].user, **validated_data) + return comment diff --git a/core/apps/tasks/serializers/label.py b/core/apps/tasks/serializers/label.py new file mode 100644 index 0000000..6b1ef9f --- /dev/null +++ b/core/apps/tasks/serializers/label.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from core.apps.tasks.models.label import Label + + +class LabelSerializer(serializers.ModelSerializer): + class Meta: + model = Label + fields = [ + 'id', 'name' + ] diff --git a/core/apps/tasks/serializers/task.py b/core/apps/tasks/serializers/task.py new file mode 100644 index 0000000..cc7b5ae --- /dev/null +++ b/core/apps/tasks/serializers/task.py @@ -0,0 +1,30 @@ +from rest_framework import serializers + +from core.apps.tasks.models.task import Task +from core.apps.accounts.serializers.user import ShortUserSerializer +from core.apps.tasks.serializers.label import LabelSerializer + + +class TaskSerializer(serializers.ModelSerializer): + labels = LabelSerializer(many=True) + + class Meta: + model = Task + fields = [ + 'id', + 'column', + 'name', + 'description', + 'priority', + 'from_date', + 'to_date', + 'labels', + 'assignees', + 'created_by' + ] + + def get_assignees(self, obj): + return ShortUserSerializer(obj.assignees.all(), many=True, context={"request": self.context['request']}) + + def get_created_by(self, obj): + return ShortUserSerializer(obj.created_by, context={"request": self.context['request']}) diff --git a/core/apps/tasks/urls.py b/core/apps/tasks/urls.py new file mode 100644 index 0000000..072a89f --- /dev/null +++ b/core/apps/tasks/urls.py @@ -0,0 +1,7 @@ +from django.urls import path, include + +from core.apps.tasks.views import task, column, comment + +urlpatterns = [ + +] diff --git a/core/apps/tasks/views/column.py b/core/apps/tasks/views/column.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/tasks/views/comment.py b/core/apps/tasks/views/comment.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/tasks/views/task.py b/core/apps/tasks/views/task.py new file mode 100644 index 0000000..e69de29 From d014f5a2fbb0dfbc24d7cb24d053dd4975d15825 Mon Sep 17 00:00:00 2001 From: xoliqberdiyev Date: Thu, 30 Apr 2026 16:02:04 +0500 Subject: [PATCH 2/2] fix --- config/urls.py | 1 + core/apps/accounts/serializers/user.py | 3 +- core/apps/tasks/serializers/board.py | 22 +++++++++++ core/apps/tasks/serializers/comment.py | 3 +- core/apps/tasks/serializers/task.py | 26 ++++++++++++- core/apps/tasks/urls.py | 31 +++++++++++++++- core/apps/tasks/views/board.py | 13 +++++++ core/apps/tasks/views/column.py | 47 ++++++++++++++++++++++++ core/apps/tasks/views/comment.py | 51 ++++++++++++++++++++++++++ core/apps/tasks/views/label.py | 22 +++++++++++ core/apps/tasks/views/task.py | 40 ++++++++++++++++++++ 11 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 core/apps/tasks/serializers/board.py create mode 100644 core/apps/tasks/views/board.py create mode 100644 core/apps/tasks/views/label.py diff --git a/config/urls.py b/config/urls.py index c43854b..e64d95b 100644 --- a/config/urls.py +++ b/config/urls.py @@ -23,6 +23,7 @@ urlpatterns = [ path("api/v1/", include("core.apps.evaluation.urls")), path("api/v1/", include("core.apps.payment.urls")), path("api/v1/", include("core.apps.chat.urls")), + path("api/v1/tasks/", include("core.apps.tasks.urls")), ] urlpatterns += [ path("admin/", admin.site.urls), diff --git a/core/apps/accounts/serializers/user.py b/core/apps/accounts/serializers/user.py index b93910c..23eee5e 100644 --- a/core/apps/accounts/serializers/user.py +++ b/core/apps/accounts/serializers/user.py @@ -63,7 +63,8 @@ class ShortUserSerializer(serializers.ModelSerializer): model = get_user_model() fields = [ 'id', - 'full_name', + 'first_name', + 'last_name', 'avatar', ] diff --git a/core/apps/tasks/serializers/board.py b/core/apps/tasks/serializers/board.py new file mode 100644 index 0000000..ad04081 --- /dev/null +++ b/core/apps/tasks/serializers/board.py @@ -0,0 +1,22 @@ +from rest_framework import serializers + +from core.apps.tasks.serializers.comment import CommentSerializer +from core.apps.tasks.serializers.task import TaskSerializer +from core.apps.tasks.models import Column, Task + + +class BoardTaskSerializer(TaskSerializer): + comments = CommentSerializer(many=True, read_only=True) + + class Meta(TaskSerializer.Meta): + TaskSerializer.Meta.fields += ['comments'] + + +class BoardSerializer(serializers.ModelSerializer): + tasks = BoardTaskSerializer(many=True, read_only=True) + + class Meta: + model = Column + fields = [ + 'id', 'name', 'tasks', + ] diff --git a/core/apps/tasks/serializers/comment.py b/core/apps/tasks/serializers/comment.py index 5500fa0..3c053af 100644 --- a/core/apps/tasks/serializers/comment.py +++ b/core/apps/tasks/serializers/comment.py @@ -17,7 +17,8 @@ class CommentSerializer(serializers.ModelSerializer): request = self.context.get('request') return { "id": obj.created_by.id, - "full_name": obj.created_by.full_name, + "first_name": obj.created_by.first_name, + "last_name": obj.created_by.last_name, "avatar": request.build_absolute_uri(obj.created_by.avatar.url) if obj.created_by.avatar else None } diff --git a/core/apps/tasks/serializers/task.py b/core/apps/tasks/serializers/task.py index cc7b5ae..bef96e7 100644 --- a/core/apps/tasks/serializers/task.py +++ b/core/apps/tasks/serializers/task.py @@ -7,6 +7,8 @@ from core.apps.tasks.serializers.label import LabelSerializer class TaskSerializer(serializers.ModelSerializer): labels = LabelSerializer(many=True) + assignees = serializers.SerializerMethodField(method_name='get_assignees') + created_by = serializers.SerializerMethodField(method_name='get_created_by') class Meta: model = Task @@ -24,7 +26,27 @@ class TaskSerializer(serializers.ModelSerializer): ] def get_assignees(self, obj): - return ShortUserSerializer(obj.assignees.all(), many=True, context={"request": self.context['request']}) + return ShortUserSerializer(obj.assignees.all(), many=True, context={"request": self.context['request']}).data def get_created_by(self, obj): - return ShortUserSerializer(obj.created_by, context={"request": self.context['request']}) + return ShortUserSerializer(obj.created_by, context={"request": self.context['request']}).data + + +class TaskCreateSerializer(serializers.ModelSerializer): + class Meta: + model = Task + fields = [ + 'id', + 'column', + 'name', + 'description', + 'priority', + 'from_date', + 'to_date', + 'labels', + 'assignees', + ] + + def create(self, validated_data): + validated_data['created_by'] = self.context['request'].user + return super().create(validated_data) diff --git a/core/apps/tasks/urls.py b/core/apps/tasks/urls.py index 072a89f..29d59af 100644 --- a/core/apps/tasks/urls.py +++ b/core/apps/tasks/urls.py @@ -1,7 +1,34 @@ from django.urls import path, include -from core.apps.tasks.views import task, column, comment +from core.apps.tasks.views import task, column, comment, label, board urlpatterns = [ - + path('column/', include( + [ + path('list/', column.ColumnListApiView.as_view()), + path('create/', column.ColumnCreateApiView.as_view()), + path('/update/', column.ColumnUpdateApiView.as_view()), + path('/delete/', column.ColumnDeleteApiView.as_view()) + ] + )), + path('label/', include( + [ + path('', label.LabelListCreateApiView.as_view()), + path('/', label.LabelRetrieveUpdateDestroyApiView.as_view()), + ] + )), + path('task/', include( + [ + path('list/', task.TaskListView.as_view()), + path('/', task.TaskDetailView.as_view()), + path('create/', task.TaskCreateView.as_view()), + ] + )), + path('comment/', include( + [ + path('', comment.CommentListCreateAPIView.as_view()), + path('/', comment.CommentDetailAPIView.as_view()), + ] + )), + path('board/', board.BoardListView.as_view()), ] diff --git a/core/apps/tasks/views/board.py b/core/apps/tasks/views/board.py new file mode 100644 index 0000000..755089e --- /dev/null +++ b/core/apps/tasks/views/board.py @@ -0,0 +1,13 @@ +from rest_framework import generics, permissions +from rest_framework.response import Response + +from core.apps.tasks.serializers.board import BoardSerializer +from core.apps.tasks.models import Column + + +class BoardListView(generics.ListAPIView): + queryset = Column.objects.order_by('id') + serializer_class = BoardSerializer + permission_classes = [permissions.IsAuthenticated] + + \ No newline at end of file diff --git a/core/apps/tasks/views/column.py b/core/apps/tasks/views/column.py index e69de29..015d269 100644 --- a/core/apps/tasks/views/column.py +++ b/core/apps/tasks/views/column.py @@ -0,0 +1,47 @@ +from django.db import transaction + +from rest_framework import generics, permissions +from rest_framework.response import Response + +from drf_spectacular.utils import extend_schema + +from core.apps.tasks.serializers.column import ColumnSerializer +from core.apps.tasks.models.column import Column + + +@extend_schema(tags=['Tasks']) +class ColumnCreateApiView(generics.GenericAPIView): + permission_classes = [permissions.IsAuthenticated] + serializer_class = ColumnSerializer + + @transaction.atomic + def post(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + column = serializer.save() + return Response(serializer.data) + + +@extend_schema(tags=['Tasks']) +class ColumnListApiView(generics.ListAPIView): + permission_classes = [permissions.IsAuthenticated] + serializer_class = ColumnSerializer + + def get_queryset(self): + return Column.objects.order_by('id') + + +@extend_schema(tags=['Tasks']) +class ColumnUpdateApiView(generics.UpdateAPIView): + permission_classes = [permissions.IsAuthenticated] + serializer_class = ColumnSerializer + lookup_field = 'id' + queryset = Column.objects.all() + + +@extend_schema(tags=['Tasks']) +class ColumnDeleteApiView(generics.DestroyAPIView): + permission_classes = [permissions.IsAuthenticated] + serializer_class = ColumnSerializer + lookup_field = 'id' + queryset = Column.objects.all() diff --git a/core/apps/tasks/views/comment.py b/core/apps/tasks/views/comment.py index e69de29..31f4d68 100644 --- a/core/apps/tasks/views/comment.py +++ b/core/apps/tasks/views/comment.py @@ -0,0 +1,51 @@ +from rest_framework import generics, permissions +from rest_framework.exceptions import PermissionDenied + +from core.apps.tasks.models.comment import Comment +from core.apps.tasks.serializers.comment import CommentSerializer, CommentCreateSerializer + + +class CommentListCreateAPIView(generics.ListCreateAPIView): + queryset = Comment.objects.all() + permission_classes = [permissions.IsAuthenticated] + + def get_serializer_class(self): + if self.request.method == 'POST': + return CommentCreateSerializer + return CommentSerializer + + def get_queryset(self): + task_id = self.request.query_params.get('task_id') + queryset = self.queryset + + if task_id: + queryset = queryset.filter(task_id=task_id) + + return queryset.order_by('-id') + + def get_serializer_context(self): + return {"request": self.request} + + +class CommentDetailAPIView(generics.RetrieveUpdateDestroyAPIView): + queryset = Comment.objects.all() + permission_classes = [permissions.IsAuthenticated] + + def get_serializer_class(self): + if self.request.method in ['PUT', 'PATCH']: + return CommentCreateSerializer + return CommentSerializer + + def get_serializer_context(self): + return {"request": self.request} + + def perform_update(self, serializer): + comment = self.get_object() + if comment.created_by != self.request.user: + raise PermissionDenied("You cannot edit this comment") + serializer.save() + + def perform_destroy(self, instance): + if instance.created_by != self.request.user: + raise PermissionDenied("You cannot delete this comment") + instance.delete() diff --git a/core/apps/tasks/views/label.py b/core/apps/tasks/views/label.py new file mode 100644 index 0000000..3cd6105 --- /dev/null +++ b/core/apps/tasks/views/label.py @@ -0,0 +1,22 @@ +from rest_framework import generics, permissions +from rest_framework.response import Response + +from drf_spectacular.utils import extend_schema + +from core.apps.tasks.serializers.label import LabelSerializer +from core.apps.tasks.models.label import Label + + +@extend_schema(tags=['Tasks']) +class LabelListCreateApiView(generics.ListCreateAPIView): + queryset = Label.objects.order_by('id') + serializer_class = LabelSerializer + permission_classes = [permissions.IsAuthenticated] + + +@extend_schema(tags=['Tasks']) +class LabelRetrieveUpdateDestroyApiView(generics.RetrieveUpdateDestroyAPIView): + queryset = Label.objects.order_by('id') + serializer_class = LabelSerializer + permission_classes = [permissions.IsAuthenticated] + lookup_field = 'id' diff --git a/core/apps/tasks/views/task.py b/core/apps/tasks/views/task.py index e69de29..22dc730 100644 --- a/core/apps/tasks/views/task.py +++ b/core/apps/tasks/views/task.py @@ -0,0 +1,40 @@ +from django.db import transaction + +from rest_framework import permissions, generics +from rest_framework.response import Response + +from drf_spectacular.utils import extend_schema + +from core.apps.tasks.models.task import Task +from core.apps.tasks.serializers.task import TaskSerializer, TaskCreateSerializer + + +@extend_schema(tags=['Tasks']) +class TaskCreateView(generics.GenericAPIView): + permission_classes = [permissions.IsAuthenticated] + serializer_class = TaskCreateSerializer + queryset = Task.objects.order_by('id') + + @transaction.atomic + def post(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + +@extend_schema(tags=['Tasks']) +class TaskListView(generics.ListAPIView): + permission_classes = [permissions.IsAuthenticated] + serializer_class = TaskSerializer + queryset = Task.objects.order_by('id') + + def serializer_context(self): + return self.serializer_class(context={"request": self.request}) + + +@extend_schema(tags=['Tasks']) +class TaskDetailView(generics.RetrieveUpdateDestroyAPIView): + permission_classes = [permissions.IsAuthenticated] + serializer_class = TaskSerializer + queryset = Task.objects.order_by('id')