feat: optimize Redis cache and implement various improvements for Task and User apps

This commit is contained in:
Chneemann 2025-03-23 10:43:26 +01:00
parent cd1e6b2a2d
commit 5ba7863913
10 changed files with 152 additions and 66 deletions

3
.gitignore vendored
View file

@ -144,3 +144,6 @@ GitHub.sublime-settings
**/migrations/** **/migrations/**
!**/migrations !**/migrations
!**/migrations/__init__.py !**/migrations/__init__.py
# Redis #
dump.rdb

View file

@ -7,6 +7,7 @@ class SubTaskInline(admin.TabularInline):
class AssignedTaskInline(admin.TabularInline): class AssignedTaskInline(admin.TabularInline):
model = AssignedTask model = AssignedTask
fields = ['user_id']
extra = 1 extra = 1
@admin.register(Task) @admin.register(Task)

View file

@ -1,16 +1,49 @@
from django.core.cache import cache from django.core.cache import cache
from django.core import serializers
from .models import Task from .models import Task
def get_cached_tasks():
cache_key = "all_tasks"
cached_tasks_json = cache.get(cache_key)
if cached_tasks_json is not None:
deserialized_objects = list(serializers.deserialize('json', cached_tasks_json))
tasks = [obj.object for obj in deserialized_objects]
return tasks
tasks = Task.objects.all()
tasks_json = serializers.serialize('json', tasks)
cache.set(cache_key, tasks_json, timeout=3600)
return tasks
def get_cached_tasks_by_status(status): def get_cached_tasks_by_status(status):
cache_key = f"tasks_by_status_{status}" cache_key = f"tasks_by_status_{status}"
cached_tasks_json = cache.get(cache_key)
cached_tasks = cache.get(cache_key) if cached_tasks_json is not None:
deserialized_objects = list(serializers.deserialize('json', cached_tasks_json))
if cached_tasks is not None: tasks = [obj.object for obj in deserialized_objects]
return cached_tasks return tasks
tasks = Task.objects.filter(status=status) tasks = Task.objects.filter(status=status)
tasks_json = serializers.serialize('json', tasks)
cache.set(cache_key, tasks, timeout=3600) cache.set(cache_key, tasks_json, timeout=3600)
return tasks return tasks
def get_cached_task_by_id(task_id):
cache_key = f"task_{task_id}"
cached_task_json = cache.get(cache_key)
if cached_task_json is not None:
try:
return list(serializers.deserialize('json', cached_task_json))[0].object
except IndexError:
return None
try:
task = Task.objects.get(pk=task_id)
task_json = serializers.serialize('json', [task])
cache.set(cache_key, task_json, timeout=3600)
return task
except Task.DoesNotExist:
return None

17
task_app/choices.py Normal file
View file

@ -0,0 +1,17 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class TaskCategory(models.TextChoices):
TECHNICAL_TASK = 'Technical Task', _('Technical Task')
USER_STORY = 'User Story', _('User Story')
class TaskPriority(models.TextChoices):
LOW = 'low', _('Low')
MEDIUM = 'medium', _('Medium')
URGENT = 'urgent', _('Urgent')
class TaskStatus(models.TextChoices):
TODO = 'todo', _('Todo')
IN_PROGRESS = 'inprogress', _('In Progress')
AWAIT_FEEDBACK = 'awaitfeedback', _('Await feedback')
DONE = 'done', _('Done')

View file

@ -1,29 +1,31 @@
import uuid 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 django.utils.translation import gettext_lazy as _
from django.core.cache import cache 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(): def generate_uuid_without_dashes():
return str(uuid.uuid4()).replace("-", "") 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.CharField(primary_key=True, default=generate_uuid_without_dashes, max_length=32, editable=False, unique=True)
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=[('Technical Task', 'Technical Task'), ('User Story', 'User Story')]) category = models.CharField(max_length=50, choices=TaskCategory.choices)
priority = models.CharField(max_length=50, choices=[('low', 'Low'), ('medium', 'Medium'), ('urgent', 'Urgent')]) priority = models.CharField(max_length=50, choices=TaskPriority.choices)
status = models.CharField(max_length=50, choices=[('todo', 'Todo'), ('inprogress', 'In Progress'), ('awaitfeedback', 'Await feedback'), ('done', 'Done')]) status = models.CharField(max_length=50, choices=TaskStatus.choices)
date = models.DateField() date = models.DateField()
creator = models.ForeignKey(User, related_name="created_tasks", on_delete=models.CASCADE) creator = models.ForeignKey(User, related_name="created_tasks", on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return self.title return self.title
class SubTask(models.Model): class SubTask(models.Model):
task = models.ForeignKey(Task, related_name="subtasks", 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)
@ -32,10 +34,30 @@ class SubTask(models.Model):
def __str__(self): def __str__(self):
return f"{self.task.title} - {self.title}" return f"{self.task.title} - {self.title}"
class AssignedTask(models.Model): class AssignedTask(models.Model):
task = models.ForeignKey(Task, related_name="assigned_users", on_delete=models.CASCADE) user_id = models.ForeignKey(User, related_name="assigned_tasks", 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): def __str__(self):
return f"{self.user.email} assigned to {self.task.title}" return f"{self.user_id} - {self.task}"
# Cache invalidation
@receiver(post_save, sender=Task)
@receiver(post_delete, sender=Task)
def clear_task_cache(sender, instance, **kwargs):
"""Invalidate cache for the affected task status and individual task"""
cache_key = f"tasks_by_status_{instance.status}"
cache.delete(cache_key)
cache_key_task = f"task_{instance.id}"
cache.delete(cache_key_task)
@receiver(post_save, sender=AssignedTask)
@receiver(post_delete, sender=AssignedTask)
def clear_assigned_task_cache(sender, instance, **kwargs):
"""Invalidate cache when a task assignment changes"""
cache_key = f"tasks_by_status_{instance.task.status}"
cache.delete(cache_key)
cache_key_task = f"task_{instance.task.id}"
cache.delete(cache_key_task)

View file

@ -6,19 +6,19 @@ from .models import Task, SubTask, AssignedTask
class SubTaskSerializer(serializers.ModelSerializer): class SubTaskSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = SubTask model = SubTask
fields = ['id', 'title', 'done'] fields = ['title', 'done']
class AssignedTaskSerializer(serializers.ModelSerializer): class AssignedTaskSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = AssignedTask model = AssignedTask
fields = ['id', 'task', 'user'] fields = ['id', 'user_id']
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer):
assignees = serializers.SerializerMethodField() assignees = serializers.SerializerMethodField()
subtasks = SubTaskSerializer(many=True, read_only=True) subtasks = SubTaskSerializer(many=True, read_only=True)
def get_assignees(self, obj): def get_assignees(self, obj):
return obj.assigned_users.all().values('id', 'user', 'task') return obj.assigned_tasks.all().values('id', 'user_id')
class Meta: class Meta:
model = Task model = Task

View file

@ -4,54 +4,48 @@ from rest_framework import status
from .models import Task, SubTask, AssignedTask from .models import Task, SubTask, AssignedTask
from .serializers import TaskSerializer, SubTaskSerializer, AssignedTaskSerializer from .serializers import TaskSerializer, SubTaskSerializer, AssignedTaskSerializer
from rest_framework.decorators import action from rest_framework.decorators import action
from .caching import get_cached_tasks_by_status from .caching import get_cached_tasks, get_cached_task_by_id, get_cached_tasks_by_status
from user_app.caching import get_cached_user from user_app.caching import get_cached_user
from django.core.cache import cache
from .choices import TaskStatus
class TaskViewSet(viewsets.ModelViewSet): class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all() queryset = Task.objects.all()
serializer_class = TaskSerializer serializer_class = TaskSerializer
def get_queryset(self): def list(self, request, *args, **kwargs):
status_param = self.request.query_params.get('status', None) status_param = request.query_params.get('status', None)
if status_param: if status_param:
tasks = get_cached_tasks_by_status(status_param) tasks = get_cached_tasks_by_status(status_param)
return tasks else:
return Task.objects.all() tasks = get_cached_tasks()
serializer = self.get_serializer(tasks, many=True)
return Response(serializer.data)
def get(self, request, status=None): def retrieve(self, request, pk=None):
tasks = get_cached_tasks_by_status(status) task = get_cached_task_by_id(pk)
task_data = [] serializer = self.get_serializer(task)
return Response(serializer.data)
for task in tasks:
creator = get_cached_user(task['creator_id'])
assignees = [get_cached_user(assignee['user_id']) for assignee in task['assigned_users']]
task_data.append({
'title': task['title'],
'description': task['description'],
'status': task['status'],
'creator': creator.first_name if creator else None,
'assignees': [assignee.first_name for assignee in assignees if assignee]
})
return Response(task_data, status=status.HTTP_200_OK)
@action(detail=True, methods=['put']) @action(detail=True, methods=['put'])
def update_status(self, request, pk=None): def update_status(self, request, pk=None):
task = self.get_object() task = self.get_object()
status = request.data.get('status') new_status = request.data.get('status')
if not status: if not new_status:
return Response({'status': 'failed', 'message': 'No status provided'}, status=400) return Response({'error': 'Status is required.'}, status=status.HTTP_400_BAD_REQUEST)
valid_statuses = ['todo', 'inprogress', 'awaitfeedback', 'done'] valid_statuses = [choice[0] for choice in TaskStatus.choices]
if status not in valid_statuses: if new_status not in valid_statuses:
return Response({'status': 'failed', 'message': f'Invalid status: {status}'}, status=400) return Response({'error': f'Invalid status. Valid statuses: {valid_statuses}'}, status=status.HTTP_400_BAD_REQUEST)
task.status = status cache.delete(f"task_{task.id}")
cache.delete(f"tasks_by_status_{task.status}")
task.status = new_status
task.save() task.save()
return Response({'status': 'updated', 'task_id': task.id}) return Response({'status': 'Status updated.'})
class SubTaskViewSet(viewsets.ModelViewSet): class SubTaskViewSet(viewsets.ModelViewSet):

View file

@ -1,19 +1,24 @@
import json
from django.core.cache import cache from django.core.cache import cache
from .models import User from .models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core import serializers
def get_cached_user(user_id): def get_cached_user(user_id):
cache_key = f"user_{user_id}" cache_key = f"user_{user_id}"
cached_user_json = cache.get(cache_key)
cached_user = cache.get(cache_key) if cached_user_json is not None:
if cached_user is not None: return json.loads(cached_user_json)
return cached_user
try: try:
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
except ObjectDoesNotExist: except ObjectDoesNotExist:
return None return None
cache.set(cache_key, user, timeout=3600) if user:
user_data = serializers.serialize('json', [user])
return user cache.set(cache_key, user_data, timeout=3600)
return json.loads(user_data)[0]['fields']
else:
return None

View file

@ -1,9 +1,12 @@
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
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
import uuid import uuid
def generate_uuid_without_dashes(): def generate_uuid_without_dashes():
return str(uuid.uuid4()).replace("-", "") 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):
@ -51,3 +54,11 @@ class User(AbstractBaseUser, PermissionsMixin):
def __str__(self): def __str__(self):
return self.email return self.email
# Cache invalidation
@receiver(post_save, sender=User)
@receiver(post_delete, sender=User)
def clear_user_cache(sender, instance, **kwargs):
"""Invalidate cache for the affected user"""
cache_key = f"user_{instance.id}"
cache.delete(cache_key)

View file

@ -6,7 +6,7 @@ from djangorestframework_camel_case.render import CamelCaseJSONRenderer
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['id', 'uId', 'first_name', 'last_name', 'email', 'phone', 'initials', 'color', 'status', 'last_login'] fields = ['id', 'first_name', 'last_name', 'email', 'phone', 'initials', 'color', 'status', 'last_login']
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer serializer_class = UserSerializer