diff --git a/auth_app/views.py b/auth_app/views.py index be3e12a..37539c3 100644 --- a/auth_app/views.py +++ b/auth_app/views.py @@ -28,6 +28,9 @@ class LoginView(APIView): if created or not token.expires_at: token.expires_at = timezone.now() + TOKEN_EXPIRATION_TIME 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) diff --git a/join/settings.py b/join/settings.py index f968825..091dd95 100644 --- a/join/settings.py +++ b/join/settings.py @@ -105,13 +105,25 @@ CACHES = { # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases +""" DATABASES = { - 'default': { + 'default': { 'ENGINE': 'django.db.backends.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 # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators diff --git a/requirements.txt b/requirements.txt index 2a62997..61fa2c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,10 @@ asgiref==3.8.1 Django==5.1.7 django-cors-headers==4.7.0 +django-environ==0.12.0 +django-redis==5.4.0 djangorestframework==3.15.2 djangorestframework-camel-case==1.4.2 +psycopg2==2.9.10 +redis==5.2.1 sqlparse==0.5.3 \ No newline at end of file diff --git a/task_app/admin.py b/task_app/admin.py index 41562a3..7d8a535 100644 --- a/task_app/admin.py +++ b/task_app/admin.py @@ -7,7 +7,7 @@ class SubTaskInline(admin.TabularInline): class AssignedTaskInline(admin.TabularInline): model = AssignedTask - fields = ['user_id'] + fields = ['user'] extra = 1 @admin.register(Task) diff --git a/task_app/models.py b/task_app/models.py index 13eff7a..e920ebb 100644 --- a/task_app/models.py +++ b/task_app/models.py @@ -2,18 +2,11 @@ import uuid from django.db import models from django.contrib.auth import get_user_model 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() -def generate_uuid_without_dashes(): - return uuid.uuid4().hex - 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) description = models.TextField(blank=True, null=True) category = models.CharField(max_length=50, choices=TaskCategory.choices) @@ -27,8 +20,8 @@ class Task(models.Model): return self.title class SubTask(models.Model): - id = models.CharField(primary_key=True, default=generate_uuid_without_dashes, max_length=32, editable=False, unique=True) - task = models.ForeignKey(Task, related_name="subtasks_task", on_delete=models.CASCADE) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + task = models.ForeignKey(Task, related_name="subtasks", on_delete=models.CASCADE) title = models.CharField(max_length=255) status = models.BooleanField(default=False) @@ -36,8 +29,8 @@ class SubTask(models.Model): return f"{self.task.title} - {self.title}" class AssignedTask(models.Model): - user_id = models.ForeignKey(User, related_name="assigned_task", on_delete=models.CASCADE) - task = models.ForeignKey(Task, 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_tasks", on_delete=models.CASCADE) def __str__(self): - return f"{self.user_id} - {self.task}" \ No newline at end of file + return f"{self.user} - {self.task}" \ No newline at end of file diff --git a/task_app/serializers.py b/task_app/serializers.py index 9a087ea..a9bfc8e 100644 --- a/task_app/serializers.py +++ b/task_app/serializers.py @@ -14,16 +14,16 @@ class AssignedTaskSerializer(serializers.ModelSerializer): fields = ['user_id'] class TaskSerializer(serializers.ModelSerializer): - assignees = serializers.SerializerMethodField() - subtasks = SubTaskSerializer(many=True, read_only=True, source="subtasks_task") + assignees = serializers.SerializerMethodField() + subtasks = SubTaskSerializer(many=True, read_only=True) def get_assignees(self, obj): - return obj.assigned_task.all().values('user_id') + return obj.assigned_tasks.all().values('user_id') class Meta: model = Task - fields = ['id', 'title', 'description', 'category', 'priority', 'status', 'date', + fields = ['id', 'title', 'description', 'category', 'priority', 'status', 'date', 'creator', 'created_at', 'subtasks', 'assignees'] - renderer_classes = [CamelCaseJSONRenderer] + renderer_classes = [CamelCaseJSONRenderer] parser_classes = [CamelCaseJSONParser] \ No newline at end of file diff --git a/task_app/services.py b/task_app/services.py new file mode 100644 index 0000000..5b1bf7b --- /dev/null +++ b/task_app/services.py @@ -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) + \ No newline at end of file diff --git a/task_app/views.py b/task_app/views.py index dfd1f2d..457de86 100644 --- a/task_app/views.py +++ b/task_app/views.py @@ -1,3 +1,4 @@ +import uuid from rest_framework import viewsets from rest_framework.response import Response from rest_framework import status @@ -10,9 +11,9 @@ from .choices import TaskStatus from rest_framework.permissions import IsAuthenticated from user_app.models import User 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): - permission_classes = [IsAuthenticated] queryset = Task.objects.all() serializer_class = TaskSerializer @@ -29,16 +30,33 @@ class TaskViewSet(viewsets.ModelViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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", []) if subtasks_data: - self._create_subtasks(subtasks_data, task) + create_subtasks(subtasks_data, task) cache.delete(f"task_{task.id}") 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): try: @@ -48,29 +66,7 @@ class TaskViewSet(viewsets.ModelViewSet): return Response({'message': 'Task deleted successfully.'}, status=status.HTTP_204_NO_CONTENT) except Task.DoesNotExist: 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']) def update_status(self, request, pk=None): task = self.get_object() diff --git a/templates/emails/password_reset_email.html b/templates/password_reset_email.html similarity index 100% rename from templates/emails/password_reset_email.html rename to templates/password_reset_email.html diff --git a/user_app/models.py b/user_app/models.py index 740f44b..59738a6 100644 --- a/user_app/models.py +++ b/user_app/models.py @@ -2,9 +2,6 @@ import uuid from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, Group, Permission from django.db import models -def generate_uuid_without_dashes(): - return uuid.uuid4().hex - class UserManager(BaseUserManager): def create_user(self, email, first_name, password=None): if not email: @@ -27,8 +24,7 @@ class UserManager(BaseUserManager): class User(AbstractBaseUser, PermissionsMixin): - uId = models.UUIDField(default=uuid.uuid4, unique=True, editable=False) - 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) first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50, blank=True, null=True) email = models.EmailField(unique=True)