19 Commits

Author SHA1 Message Date
github-actions[bot]
f53125cfdc 🔄 Update image to 147 [CI SKIP] 2026-04-30 11:05:42 +00:00
65ab51e652 Merge pull request 'update' (#128) from shaxob into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m11s
Reviewed-on: #128
2026-04-30 11:03:51 +00:00
2997810fae Merge pull request 'behruz' (#129) from behruz into main
Some checks failed
Deploy to Production / build-and-deploy (push) Has been cancelled
Reviewed-on: #129
2026-04-30 11:03:44 +00:00
xoliqberdiyev
d014f5a2fb fix 2026-04-30 16:02:04 +05:00
xoliqberdiyev
7d49929772 Merge branch 'main' of https://gitea.felixits.uz/sifatbaho/backend-v1 into behruz 2026-04-30 15:22:18 +05:00
Shaxobff
c29546a04b update 2026-04-30 11:11:12 +05:00
xoliqberdiyev
b39c080de3 add tasks app 2026-04-29 18:40:51 +05:00
github-actions[bot]
7ad385af94 🔄 Update image to 145 [CI SKIP] 2026-04-29 12:30:39 +00:00
3781ce29e5 Merge pull request 'shaxob' (#127) from shaxob into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m19s
Reviewed-on: #127
2026-04-29 12:28:42 +00:00
Shaxobff
db7e34c1c2 update 2026-04-29 16:12:12 +05:00
Shaxobff
1cb9551e81 update 2026-04-29 14:21:33 +05:00
Shaxobff
51b30c2cc4 update 2026-04-29 11:57:23 +05:00
Shaxobff
dc4c98bfc9 update 2026-04-29 11:18:50 +05:00
github-actions[bot]
abed9e59b4 🔄 Update image to 144 [CI SKIP] 2026-04-28 13:41:10 +00:00
f238c92a09 Merge pull request 'fix' (#125) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m11s
Reviewed-on: #125
2026-04-28 13:32:46 +00:00
xoliqberdiyev
113f2da120 fix 2026-04-28 18:32:24 +05:00
github-actions[bot]
99b265f68f 🔄 Update image to 143 [CI SKIP] 2026-04-28 13:19:46 +00:00
c5d60e799c Merge pull request 'fix' (#124) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 2m17s
Reviewed-on: #124
2026-04-28 13:17:52 +00:00
xoliqberdiyev
7829c9c625 fix 2026-04-28 18:17:21 +05:00
43 changed files with 878 additions and 35 deletions

View File

@@ -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 += [
]

View File

@@ -186,5 +186,57 @@ PAGES = [
"link": reverse_lazy("admin:shared_villagemodel_changelist"),
},
]
},
{
"title": _("Ruxsatlar"),
"separator": True,
"items": [
{
"title": _("Ruxsatlar"),
"icon": "attach_file",
"link": reverse_lazy("admin:accounts_permission_changelist"),
},
{
"title": _("Sahifa uchun ruxsatlar"),
"icon": "attach_file",
"link": reverse_lazy("admin:accounts_permissiontotab_changelist"),
},
{
"title": _("Actionlar uchun ruxsatlar"),
"icon": "attach_file",
"link": reverse_lazy("admin:accounts_permissiontoaction_changelist"),
},
{
"title": _("Role"),
"icon": "attach_file",
"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"),
},
]
}
]

View File

@@ -13,7 +13,7 @@ from config.env import env
def home(request):
return HttpResponse("OK: #f7be3be5d25bd807933c20ccd8ef0dc195aab501")
return HttpResponse("OK: #65ab51e65224a92a4b6d488d3e8f9b21d3256876")
urlpatterns = [
@@ -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),

View File

@@ -6,13 +6,16 @@ from core.apps.accounts.models.permission import (
Role,
)
@admin.register(PermissionToAction)
class PermissionToActionAdmin(admin.ModelAdmin):
list_display = ("id", "name", "code", "created_at")
search_fields = ("name", "code")
list_filter = ("created_at",)
ordering = ("-id",)
fieldsets = (
("Asosiy", {
"fields": ("name", "code"),
}),
)
@admin.register(PermissionToTab)
@@ -20,8 +23,15 @@ class PermissionToTabAdmin(admin.ModelAdmin):
list_display = ("id", "name", "code", "created_at")
search_fields = ("name", "code")
filter_horizontal = ("permission_to_actions",)
list_filter = ("created_at",)
ordering = ("-id",)
fieldsets = (
("Asosiy", {
"fields": ("name", "code"),
}),
("Harakatlar", {
"fields": ("permission_to_actions",),
}),
)
@admin.register(Permission)
@@ -29,16 +39,39 @@ class PermissionAdmin(admin.ModelAdmin):
list_display = ("id", "name", "code", "created_at")
search_fields = ("name", "code")
filter_horizontal = ("permission_tabs",)
list_filter = ("created_at",)
ordering = ("-id",)
fieldsets = (
("Asosiy", {
"fields": ("name", "code"),
}),
("Boglanishlar", {
"fields": ("permission_tabs",),
}),
)
@admin.register(Role)
class RoleAdmin(admin.ModelAdmin):
list_display = ("id", "name")
search_fields = ("name",)
filter_horizontal = (
"permissions",
"permission_to_tabs",
"permission_to_actions",
)
)
fieldsets = (
("Asosiy ma'lumotlar", {
"fields": ("name", "comment"),
}),
("Sahifa ruxsatlari", {
"fields": ("permissions",),
}),
("Bolim ruxsatlari", {
"fields": ("permission_to_tabs",),
}),
("Harakat ruxsatlari", {
"fields": ("permission_to_actions",),
}),
)

View File

@@ -0,0 +1,15 @@
from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import BasePermission
from core.apps.accounts.choices import RoleChoice
class IsAdminRole(BasePermission):
def has_permission(self, request, view):
if not request.user.is_authenticated:
return False
if request.user.role != RoleChoice.ADMIN:
raise PermissionDenied("Only admin can access this")
return True

View File

@@ -10,7 +10,7 @@ class PermissionToActionSerializer(serializers.ModelSerializer):
class PermissionToTabSerializer(serializers.ModelSerializer):
permission_to_actions = PermissionToActionSerializer(many=True)
permission_to_actions = PermissionToActionSerializer(many=True, read_only=True)
class Meta:
model = PermissionToTab
@@ -18,17 +18,35 @@ class PermissionToTabSerializer(serializers.ModelSerializer):
class PermissionSerializer(serializers.ModelSerializer):
permission_tabs = PermissionToTabSerializer(many=True)
permission_tabs = PermissionToTabSerializer(many=True, read_only=True)
class Meta:
model = Permission
fields = ['id', 'name', 'permission_tabs']
class PermissionToActionListSerializer(serializers.ModelSerializer):
class Meta:
model = PermissionToAction
fields = ['id', 'name']
class PermissionToTabListSerializer(serializers.ModelSerializer):
class Meta:
model = PermissionToTab
fields = ['id', 'name']
class PermissionListSerializer(serializers.ModelSerializer):
class Meta:
model = Permission
fields = ['id', 'name']
class RoleListSerializer(serializers.ModelSerializer):
permissions = PermissionSerializer(many=True)
permission_to_tabs = PermissionToTabSerializer(many=True)
permission_to_actions = PermissionToActionSerializer(many=True)
permissions = PermissionListSerializer(many=True)
permission_to_tabs = PermissionToTabListSerializer(many=True)
permission_to_actions = PermissionToActionListSerializer(many=True)
class Meta:
model = Role

View File

@@ -54,4 +54,22 @@ class UserCreateSerializer(serializers.ModelSerializer):
"first_name",
"last_name",
"password",
"role"]
"role"
]
class ShortUserSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = [
'id',
'first_name',
'last_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

View File

@@ -9,7 +9,7 @@ from .views import RegisterView, ResetPasswordView, MeView, ChangePasswordView,
from rest_framework.routers import DefaultRouter
from .views.permission import PermissionToActionViewSet, PermissionToTabViewSet, PermissionViewSet, RoleViewSet
from core.apps.accounts.views.user import DeleteAdminUserApiView
from core.apps.accounts.views.user import DeleteAdminUserApiView, UserDetailAPIView
router = DefaultRouter()
router.register("auth", RegisterView, basename="auth")
@@ -27,10 +27,11 @@ urlpatterns = [
path("", include(router.urls)),
path("auth/token/", jwt_views.TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("auth/token/verify/", jwt_views.TokenVerifyView.as_view(), name="token_verify"),
path("auth/token/refresh/",jwt_views.TokenRefreshView.as_view()),
path("auth/token/refresh/", jwt_views.TokenRefreshView.as_view()),
path("user/list/", UserListApiView.as_view(), name="user-list"),
path("admin-user/list/", AdminUserListApiView.as_view(), name="admin-user-list"),
path("admin/create/", AdminCreateAPIView.as_view(), name="user-create"),
path("admin/update/<int:pk>/", AdminUpdateAPIView.as_view(), name="user-update"),
path('user/admin/<int:pk>/delete/', DeleteAdminUserApiView.as_view(), name='user-delete')
path('user/admin/<int:pk>/delete/', DeleteAdminUserApiView.as_view(), name='user-delete'),
path('user/<int:pk>/', UserDetailAPIView.as_view(), name='user-detail'),
]

View File

@@ -9,6 +9,8 @@ from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from core.apps.accounts.choices.user import RoleChoice
from core.apps.accounts.models import Role
from core.apps.accounts.serializers.permission import RoleListSerializer
from core.apps.accounts.serializers.user import UserSerializer, AdminUserSerializer, UserCreateSerializer
User = get_user_model()
@@ -64,7 +66,7 @@ class AdminCreateAPIView(APIView):
return Response(serializer.data, status=201)
@extend_schema(tags=['User'],)
@extend_schema(tags=['User'], )
class AdminUpdateAPIView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = UserCreateSerializer
@@ -88,10 +90,29 @@ class DeleteAdminUserApiView(APIView):
if request.user.role != RoleChoice.SUPERUSER:
return Response({'detail': 'Forbidden'}, status=403)
user = get_object_or_404(User, pk=pk)
if user.role != RoleChoice.ADMIN:
return Response({'detail': 'This user is not an admin'}, status=400)
user.delete()
return Response(status=204)
class UserDetailAPIView(generics.RetrieveAPIView):
permission_classes = [IsAuthenticated]
serializer_class = UserSerializer
lookup_field = 'id'
class AdminPermissionsAPIView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
queryset = User.objects.all()
def get(self, request):
if request.user.role.name != RoleChoice.ADMIN:
return Response({'detail': 'Forbidden'}, status=403)
admin_role = Role.objects.get(name=RoleChoice.ADMIN)
serializer = RoleListSerializer(admin_role)
return Response(serializer.data)

View File

@@ -321,6 +321,7 @@ class AutoEvaluationAppraisersSerializer(serializers.Serializer):
data['users'] = users
return data
class AutoEvaluationSerializer(serializers.Serializer):
brand = serializers.CharField()
brand_model = serializers.CharField()
@@ -329,4 +330,60 @@ class AutoEvaluationSerializer(serializers.Serializer):
transmission = serializers.CharField()
condition = serializers.CharField()
fuel_type = serializers.CharField()
mileage = serializers.CharField()
mileage = serializers.CharField()
class AutoEvaluationModelSerializer(serializers.ModelSerializer):
user = serializers.StringRelatedField(read_only=True)
appraisers = serializers.PrimaryKeyRelatedField(
many=True,
queryset=User.objects.all(),
required=False
)
class Meta:
model = AutoEvaluationModel
fields = ("tex_passport_file",
"registration_number",
"contract_date",
"object_inspection_date",
"rate_date",
"rate_report_date",
"object_type",
"object_owner_type",
"object_owner_individual_person_f_name",
"object_owner_individual_person_l_name",
"object_owner_individual_person_p_name",
"object_owner_individual_person_passport_num",
"object_owner_legal_entity",
"object_owner_legal_inn",
"value_determined",
"rate_type",
"tex_passport_serie_num",
"tex_passport_gived_date",
"tex_passport_gived_location",
"car_type",
"car_wheel",
"car_brand",
"car_model",
"car_number",
"manufacture_year",
"car_dvigatel_number",
"car_color",
"rating_goal",
"status",
"is_archived",
"created_at",
"updated_at",
)
read_only_fields = (
"id",
"created_at",
"updated_at",
)

View File

@@ -128,3 +128,42 @@ class CreateQuickevaluationSerializer(serializers.ModelSerializer):
return super().create(validated_data)
class QuickEvaluationModelSerializer(serializers.ModelSerializer):
class Meta:
model = QuickEvaluationModel
fields = (
"id",
"created_by",
"brand",
"marka",
"car_position",
"body_type",
"color",
"fuel_type",
"state_car",
"tex_passport_serie_num",
"tech_passport_issued_date",
"tech_passport_issued_place",
"car_type",
"distance_covered",
"vin_number",
"car_number",
"car_manufactured_date",
"engine_number",
"estimated_price",
"status",
"is_archive",
"created_at",
"updated_at",
)
read_only_fields = (
"id",
"created_at",
"updated_at",
)

View File

@@ -37,6 +37,7 @@ urlpatterns = [
# Quick Evaluation
path('quick-evaluation/', include(
[
path("admin/", views.AdminQuickEvalAPIView.as_view(), name="quick-evaluation"),
path(
'archive/', include(
[
@@ -51,6 +52,7 @@ urlpatterns = [
# Auto Evaluation
path("auto-evaluation/", include(
[
path("admin/", views.AdminEvaluationsAPIView.as_view(), name="admin-evaluations"),
path('archive/', include(
[
path('<int:pk>/', views.AutoEvaluationArchiveAPIView.as_view()),

View File

@@ -1,10 +1,9 @@
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django_core.mixins import BaseViewSetMixin
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, OpenApiParameter
from rest_framework import generics
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.generics import GenericAPIView, ListAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
@@ -12,10 +11,12 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from core.apps.accounts.permissions import IsAdminRole
from core.apps.accounts.serializers.user import UserSerializer
from core.apps.evaluation.filters.auto import AutoevaluationFilter
from core.apps.evaluation.models import AutoEvaluationModel
from core.apps.evaluation.serializers import auto as serializers
from core.apps.evaluation.serializers import auto as serializers, AutoEvaluationModelSerializer
@extend_schema(tags=["AutoEvaluation"])
class AutoEvaluationView(BaseViewSetMixin, ModelViewSet):
@@ -175,3 +176,17 @@ class AutoEvaluationArchiveAPIView(APIView):
},
status=200
)
@extend_schema(tags=["AutoEvaluation"])
class AdminEvaluationsAPIView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, IsAdminRole]
queryset = AutoEvaluationModel.objects.all()
serializer_class = AutoEvaluationModel
def get(self, request):
auto_eval = AutoEvaluationModel.objects.filter(
created_by=self.request.user
).select_related('appraisers').distinct()
serializer = AutoEvaluationModelSerializer(auto_eval, many=True)
return Response(serializer.data)

View File

@@ -1,17 +1,13 @@
# django
from django.shortcuts import get_object_or_404
# django core
from django_core.mixins import BaseViewSetMixin
# django filters
from django_filters.rest_framework import DjangoFilterBackend
# swagger
from drf_spectacular.utils import extend_schema
# rest framework
from rest_framework import status
from rest_framework import status, generics
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.generics import ListAPIView
from rest_framework.parsers import FormParser, MultiPartParser
@@ -20,10 +16,11 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from core.apps.accounts.permissions import IsAdminRole
# core apps
from core.apps.evaluation.filters.quick import QuickevaluationFilter
from core.apps.evaluation.models import QuickEvaluationModel
from core.apps.evaluation.serializers import quick as serializers
from core.apps.evaluation.serializers import quick as serializers, QuickEvaluationModelSerializer
@extend_schema(tags=["QuickEvaluation"])
@@ -87,3 +84,18 @@ class QuickEvaluationArchivedListAPIView(ListAPIView):
def get_queryset(self):
return QuickEvaluationModel.objects.filter(is_archive=True)
@extend_schema(tags=["QuickEvaluation"])
class AdminQuickEvalAPIView(generics.GenericAPIView):
permission_classes = [IsAuthenticated, IsAdminRole]
queryset = QuickEvaluationModel.objects.all()
serializer_class = QuickEvaluationModelSerializer
def get(self, request):
quick_eval = QuickEvaluationModel.objects.filter(
created_by=self.request.user
).select_related('created_by').distinct()
serializer = QuickEvaluationModelSerializer(quick_eval, many=True)
return Response(serializer.data)

View File

View File

@@ -0,0 +1,4 @@
from .column import *
from .comment import *
from .task import *
from .label import *

View File

@@ -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',)

View File

@@ -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')

View File

@@ -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',)

View File

@@ -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')

8
core/apps/tasks/apps.py Normal file
View File

@@ -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

View File

@@ -0,0 +1,6 @@
from django.db import models
class MessageChoice(models.TextChoices):
FILE = "file", "File"
TEXT = "text", "Text"

View File

@@ -0,0 +1,7 @@
from django.db import models
class PriorityChoice(models.TextChoices):
LOW = "low", "Low"
MEDIUM = "medium", "Medium"
HIGH = "high", "High"

View File

@@ -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,
},
),
]

View File

@@ -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),
),
]

View File

View File

@@ -0,0 +1,4 @@
from .column import *
from .comment import *
from .task import *
from .label import *

View File

@@ -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

View File

@@ -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}"

View File

@@ -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

View File

@@ -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}"

View File

@@ -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',
]

View File

@@ -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'
]

View File

@@ -0,0 +1,44 @@
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,
"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
}
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

View File

@@ -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'
]

View File

@@ -0,0 +1,52 @@
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)
assignees = serializers.SerializerMethodField(method_name='get_assignees')
created_by = serializers.SerializerMethodField(method_name='get_created_by')
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']}).data
def get_created_by(self, obj):
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)

34
core/apps/tasks/urls.py Normal file
View File

@@ -0,0 +1,34 @@
from django.urls import path, include
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('<int:id>/update/', column.ColumnUpdateApiView.as_view()),
path('<int:id>/delete/', column.ColumnDeleteApiView.as_view())
]
)),
path('label/', include(
[
path('', label.LabelListCreateApiView.as_view()),
path('<int:id>/', label.LabelRetrieveUpdateDestroyApiView.as_view()),
]
)),
path('task/', include(
[
path('list/', task.TaskListView.as_view()),
path('<int:id>/', task.TaskDetailView.as_view()),
path('create/', task.TaskCreateView.as_view()),
]
)),
path('comment/', include(
[
path('', comment.CommentListCreateAPIView.as_view()),
path('<int:pk>/', comment.CommentDetailAPIView.as_view()),
]
)),
path('board/', board.BoardListView.as_view()),
]

View File

@@ -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]

View File

@@ -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()

View File

@@ -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()

View File

@@ -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'

View File

@@ -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')

View File

@@ -84,7 +84,7 @@ services:
max-file: "5"
web:
image: husanjon/sifatbaho:142
image: husanjon/sifatbaho:147
env_file:
- .env
environment:
@@ -129,7 +129,7 @@ services:
max-file: "5"
celery:
image: husanjon/sifatbaho:142
image: husanjon/sifatbaho:147
env_file:
- .env
environment: