feat: integrate and migrate PostgreSQL in Django Rest; extract functions in task_app to services.py, enable adding/removing assignees and subtasks
This commit is contained in:
parent
81d1334013
commit
d3227e9a76
10 changed files with 132 additions and 52 deletions
|
|
@ -29,6 +29,9 @@ class LoginView(APIView):
|
||||||
token.expires_at = timezone.now() + TOKEN_EXPIRATION_TIME
|
token.expires_at = timezone.now() + TOKEN_EXPIRATION_TIME
|
||||||
token.save()
|
token.save()
|
||||||
|
|
||||||
|
user.last_login = timezone.now()
|
||||||
|
user.save(update_fields=['last_login'])
|
||||||
|
|
||||||
return Response({'token': token.key, 'user_id': user.id}, status=status.HTTP_200_OK)
|
return Response({'token': token.key, 'user_id': user.id}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
|
|
|
||||||
|
|
@ -105,13 +105,25 @@ CACHES = {
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
|
||||||
|
|
||||||
|
"""
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': 'join_db',
|
||||||
|
'USER': 'chneemann',
|
||||||
|
'PASSWORD': '91&yWMr6',
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
asgiref==3.8.1
|
asgiref==3.8.1
|
||||||
Django==5.1.7
|
Django==5.1.7
|
||||||
django-cors-headers==4.7.0
|
django-cors-headers==4.7.0
|
||||||
|
django-environ==0.12.0
|
||||||
|
django-redis==5.4.0
|
||||||
djangorestframework==3.15.2
|
djangorestframework==3.15.2
|
||||||
djangorestframework-camel-case==1.4.2
|
djangorestframework-camel-case==1.4.2
|
||||||
|
psycopg2==2.9.10
|
||||||
|
redis==5.2.1
|
||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
|
|
@ -7,7 +7,7 @@ class SubTaskInline(admin.TabularInline):
|
||||||
|
|
||||||
class AssignedTaskInline(admin.TabularInline):
|
class AssignedTaskInline(admin.TabularInline):
|
||||||
model = AssignedTask
|
model = AssignedTask
|
||||||
fields = ['user_id']
|
fields = ['user']
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
@admin.register(Task)
|
@admin.register(Task)
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,11 @@ import uuid
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from .choices import TaskCategory, TaskPriority, TaskStatus
|
from .choices import TaskCategory, TaskPriority, TaskStatus
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from django.db.models.signals import post_save, post_delete
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
def generate_uuid_without_dashes():
|
|
||||||
return uuid.uuid4().hex
|
|
||||||
|
|
||||||
class Task(models.Model):
|
class Task(models.Model):
|
||||||
id = models.CharField(primary_key=True, default=generate_uuid_without_dashes, max_length=32, editable=False, unique=True)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
category = models.CharField(max_length=50, choices=TaskCategory.choices)
|
category = models.CharField(max_length=50, choices=TaskCategory.choices)
|
||||||
|
|
@ -27,8 +20,8 @@ class Task(models.Model):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
class SubTask(models.Model):
|
class SubTask(models.Model):
|
||||||
id = models.CharField(primary_key=True, default=generate_uuid_without_dashes, max_length=32, editable=False, unique=True)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
task = models.ForeignKey(Task, related_name="subtasks_task", on_delete=models.CASCADE)
|
task = models.ForeignKey(Task, related_name="subtasks", on_delete=models.CASCADE)
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
status = models.BooleanField(default=False)
|
status = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
@ -36,8 +29,8 @@ class SubTask(models.Model):
|
||||||
return f"{self.task.title} - {self.title}"
|
return f"{self.task.title} - {self.title}"
|
||||||
|
|
||||||
class AssignedTask(models.Model):
|
class AssignedTask(models.Model):
|
||||||
user_id = models.ForeignKey(User, related_name="assigned_task", on_delete=models.CASCADE)
|
user = models.ForeignKey(User, related_name="assigned_tasks", on_delete=models.CASCADE)
|
||||||
task = models.ForeignKey(Task, related_name="assigned_task", on_delete=models.CASCADE)
|
task = models.ForeignKey(Task, related_name="assigned_tasks", on_delete=models.CASCADE)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user_id} - {self.task}"
|
return f"{self.user} - {self.task}"
|
||||||
|
|
@ -15,10 +15,10 @@ class AssignedTaskSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class TaskSerializer(serializers.ModelSerializer):
|
class TaskSerializer(serializers.ModelSerializer):
|
||||||
assignees = serializers.SerializerMethodField()
|
assignees = serializers.SerializerMethodField()
|
||||||
subtasks = SubTaskSerializer(many=True, read_only=True, source="subtasks_task")
|
subtasks = SubTaskSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
def get_assignees(self, obj):
|
def get_assignees(self, obj):
|
||||||
return obj.assigned_task.all().values('user_id')
|
return obj.assigned_tasks.all().values('user_id')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Task
|
model = Task
|
||||||
|
|
|
||||||
76
task_app/services.py
Normal file
76
task_app/services.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import uuid
|
||||||
|
from rest_framework.exceptions import APIException
|
||||||
|
from .models import SubTask, AssignedTask
|
||||||
|
from user_app.models import User
|
||||||
|
|
||||||
|
def create_or_update_subtasks(subtask_data, parent_task):
|
||||||
|
current_subtasks = SubTask.objects.filter(task=parent_task)
|
||||||
|
current_subtask_ids = {str(sub.id): sub for sub in current_subtasks} # Speichere Objekte zur einfachen Aktualisierung
|
||||||
|
|
||||||
|
new_subtasks_to_create = []
|
||||||
|
subtasks_to_update = []
|
||||||
|
new_subtask_ids = set()
|
||||||
|
|
||||||
|
for sub_data in subtask_data:
|
||||||
|
sub_id = sub_data.get('id')
|
||||||
|
new_subtask_ids.add(str(sub_id))
|
||||||
|
if sub_id and str(sub_id) in current_subtask_ids:
|
||||||
|
subtask = current_subtask_ids[str(sub_id)]
|
||||||
|
subtask.title = sub_data.get('title', subtask.title)
|
||||||
|
subtask.status = sub_data.get('status', subtask.status)
|
||||||
|
subtasks_to_update.append(subtask)
|
||||||
|
else:
|
||||||
|
new_subtasks_to_create.append(
|
||||||
|
SubTask(
|
||||||
|
title=sub_data.get('title'),
|
||||||
|
status=sub_data.get('status'),
|
||||||
|
task=parent_task
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
subtasks_to_delete = [
|
||||||
|
sub_id for sub_id in current_subtask_ids if sub_id not in new_subtask_ids
|
||||||
|
]
|
||||||
|
SubTask.objects.filter(id__in=subtasks_to_delete, task=parent_task).delete()
|
||||||
|
|
||||||
|
SubTask.objects.bulk_create(new_subtasks_to_create)
|
||||||
|
SubTask.objects.bulk_update(subtasks_to_update, ['title', 'status'])
|
||||||
|
|
||||||
|
def create_or_update_assignees(assignee_data, task):
|
||||||
|
new_assignee_ids = {str(user["user_id"]) for user in assignee_data if "user_id" in user}
|
||||||
|
|
||||||
|
current_assignees = AssignedTask.objects.filter(task=task).values_list('user_id', flat=True)
|
||||||
|
current_assignee_ids = {str(assignee_id) for assignee_id in current_assignees}
|
||||||
|
|
||||||
|
assignees_to_remove = current_assignee_ids - new_assignee_ids
|
||||||
|
assignees_to_add = new_assignee_ids - current_assignee_ids
|
||||||
|
|
||||||
|
AssignedTask.objects.filter(task=task, user_id__in=assignees_to_remove).delete()
|
||||||
|
|
||||||
|
users_to_add = User.objects.filter(id__in=assignees_to_add)
|
||||||
|
AssignedTask.objects.bulk_create(
|
||||||
|
[AssignedTask(user=user, task=task) for user in users_to_add]
|
||||||
|
)
|
||||||
|
|
||||||
|
def assign_users_to_task(user_ids, task):
|
||||||
|
user_ids = [uuid.UUID(user["user_id"]) for user in user_ids if "user_id" in user]
|
||||||
|
|
||||||
|
users = User.objects.filter(id__in=user_ids)
|
||||||
|
found_user_ids = set(users.values_list('id', flat=True))
|
||||||
|
missing_users = set(user_ids) - found_user_ids
|
||||||
|
|
||||||
|
if missing_users:
|
||||||
|
raise APIException(f"Users not found: {', '.join(map(str, missing_users))}") # Konvertiere UUIDs zurück zu Strings für die Fehlermeldung
|
||||||
|
|
||||||
|
AssignedTask.objects.bulk_create(
|
||||||
|
[AssignedTask(user=user, task=task) for user in users]
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_subtasks(subtask_data, parent_task):
|
||||||
|
if subtask_data:
|
||||||
|
subtasks = [
|
||||||
|
SubTask(title=sub['title'], status=sub['status'], task=parent_task)
|
||||||
|
for sub in subtask_data
|
||||||
|
]
|
||||||
|
SubTask.objects.bulk_create(subtasks)
|
||||||
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import uuid
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
@ -10,9 +11,9 @@ from .choices import TaskStatus
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from user_app.models import User
|
from user_app.models import User
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
|
from .services import create_or_update_subtasks, create_or_update_assignees, assign_users_to_task, create_subtasks
|
||||||
|
|
||||||
class TaskViewSet(viewsets.ModelViewSet):
|
class TaskViewSet(viewsets.ModelViewSet):
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
queryset = Task.objects.all()
|
queryset = Task.objects.all()
|
||||||
serializer_class = TaskSerializer
|
serializer_class = TaskSerializer
|
||||||
|
|
||||||
|
|
@ -29,17 +30,34 @@ class TaskViewSet(viewsets.ModelViewSet):
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
task = serializer.save()
|
task = serializer.save()
|
||||||
self._assign_users_to_task(request.data.get('assignees', []), task)
|
assign_users_to_task(request.data.get('assignees', []), task)
|
||||||
|
|
||||||
subtasks_data = request.data.get("subtasks", [])
|
subtasks_data = request.data.get("subtasks", [])
|
||||||
|
|
||||||
if subtasks_data:
|
if subtasks_data:
|
||||||
self._create_subtasks(subtasks_data, task)
|
create_subtasks(subtasks_data, task)
|
||||||
|
|
||||||
cache.delete(f"task_{task.id}")
|
cache.delete(f"task_{task.id}")
|
||||||
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
task = self.get_object()
|
||||||
|
serializer = self.get_serializer(task, data=request.data, partial=True)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
task = serializer.save()
|
||||||
|
|
||||||
|
assignees_data = request.data.get('assignees', [])
|
||||||
|
create_or_update_assignees(assignees_data, task)
|
||||||
|
|
||||||
|
subtasks_data = request.data.get('subtasks', [])
|
||||||
|
create_or_update_subtasks(subtasks_data, task)
|
||||||
|
|
||||||
|
cache.delete(f"task_{task.id}")
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def destroy(self, request, pk=None):
|
def destroy(self, request, pk=None):
|
||||||
try:
|
try:
|
||||||
task = Task.objects.get(pk=pk)
|
task = Task.objects.get(pk=pk)
|
||||||
|
|
@ -49,28 +67,6 @@ class TaskViewSet(viewsets.ModelViewSet):
|
||||||
except Task.DoesNotExist:
|
except Task.DoesNotExist:
|
||||||
return Response({'error': 'Task not found.'}, status=status.HTTP_404_NOT_FOUND)
|
return Response({'error': 'Task not found.'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
def _create_subtasks(self, subtask_data, parent_task):
|
|
||||||
if subtask_data:
|
|
||||||
subtasks = [
|
|
||||||
SubTask(title=sub['title'], status=sub['status'], task=parent_task)
|
|
||||||
for sub in subtask_data
|
|
||||||
]
|
|
||||||
SubTask.objects.bulk_create(subtasks)
|
|
||||||
|
|
||||||
def _assign_users_to_task(self, user_ids, task):
|
|
||||||
user_ids = [user["user_id"] for user in user_ids if "user_id" in user]
|
|
||||||
|
|
||||||
users = User.objects.filter(id__in=user_ids)
|
|
||||||
found_user_ids = set(users.values_list('id', flat=True))
|
|
||||||
missing_users = set(user_ids) - found_user_ids
|
|
||||||
|
|
||||||
if missing_users:
|
|
||||||
raise APIException(f"Users not found: {', '.join(missing_users)}")
|
|
||||||
|
|
||||||
AssignedTask.objects.bulk_create(
|
|
||||||
[AssignedTask(user_id=user, task=task) for user in users]
|
|
||||||
)
|
|
||||||
|
|
||||||
@action(detail=True, methods=['patch'])
|
@action(detail=True, methods=['patch'])
|
||||||
def update_status(self, request, pk=None):
|
def update_status(self, request, pk=None):
|
||||||
task = self.get_object()
|
task = self.get_object()
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@ import uuid
|
||||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, Group, Permission
|
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, Group, Permission
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
def generate_uuid_without_dashes():
|
|
||||||
return uuid.uuid4().hex
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
class UserManager(BaseUserManager):
|
||||||
def create_user(self, email, first_name, password=None):
|
def create_user(self, email, first_name, password=None):
|
||||||
if not email:
|
if not email:
|
||||||
|
|
@ -27,8 +24,7 @@ class UserManager(BaseUserManager):
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractBaseUser, PermissionsMixin):
|
class User(AbstractBaseUser, PermissionsMixin):
|
||||||
uId = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
id = models.CharField(primary_key=True, default=generate_uuid_without_dashes, max_length=32,editable=False, unique=True)
|
|
||||||
first_name = models.CharField(max_length=50)
|
first_name = models.CharField(max_length=50)
|
||||||
last_name = models.CharField(max_length=50, blank=True, null=True)
|
last_name = models.CharField(max_length=50, blank=True, null=True)
|
||||||
email = models.EmailField(unique=True)
|
email = models.EmailField(unique=True)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue