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