diff --git a/backend/core/settings.py b/backend/core/settings.py index 2c6afa9..65fc5f7 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -47,6 +47,8 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', + 'tasks', + 'users', 'rest_framework', 'corsheaders', diff --git a/backend/core/urls.py b/backend/core/urls.py index 5e07d78..78f3e22 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -20,5 +20,6 @@ from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('users.urls')), + path('api/', include('tasks.urls')), path('accounts/', include('allauth.urls')), ] \ No newline at end of file diff --git a/backend/tasks/__init__.py b/backend/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tasks/admin.py b/backend/tasks/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/tasks/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/tasks/apps.py b/backend/tasks/apps.py new file mode 100644 index 0000000..3ff3ab3 --- /dev/null +++ b/backend/tasks/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TasksConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'tasks' diff --git a/backend/tasks/migrations/0001_initial.py b/backend/tasks/migrations/0001_initial.py new file mode 100644 index 0000000..e60ea87 --- /dev/null +++ b/backend/tasks/migrations/0001_initial.py @@ -0,0 +1,83 @@ +# Generated by Django 4.2.6 on 2023-10-28 15:50 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Reminder', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('startDate', models.DateField(blank=True, null=True)), + ('time', models.DateTimeField()), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='UserNotification', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('LEVEL_UP', 'Level Up'), ('DEATH', 'Death')], max_length=255)), + ('data', models.JSONField(default=dict)), + ('seen', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='Transaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('currency', models.CharField(choices=[('gold', 'Gold')], max_length=12)), + ('transaction_type', models.CharField(choices=[('buy_gold', 'Buy Gold'), ('spend', 'Spend'), ('debug', 'Debug'), ('force_update_gold', 'Force Update Gold')], max_length=24)), + ('description', models.TextField(blank=True)), + ('amount', models.FloatField(default=0)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Task', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(choices=[('daily', 'Daily'), ('habit', 'Habit'), ('todo', 'Todo'), ('Long Term Goal', 'Long Term Goal')], default='habit', max_length=15)), + ('title', models.TextField()), + ('notes', models.TextField(default='')), + ('completed', models.BooleanField(default=False)), + ('exp', models.FloatField(default=0)), + ('priority', models.FloatField(default=1, validators=[django.core.validators.MinValueValidator(0.1), django.core.validators.MaxValueValidator(2)])), + ('difficulty', models.PositiveSmallIntegerField(choices=[(1, 'Easy'), (2, 'Normal'), (3, 'Hard'), (4, 'Very Hard'), (5, 'Devil')], unique=True)), + ('attribute', models.CharField(choices=[('str', 'Strength'), ('int', 'Intelligence'), ('end', 'Endurance'), ('per', 'Perception'), ('luck', 'Luck')], default='str', max_length=15)), + ('challenge', models.BooleanField(default=False)), + ('fromSystem', models.BooleanField(default=False)), + ('creation_date', models.DateTimeField(auto_now_add=True)), + ('last_update', models.DateTimeField(auto_now=True)), + ('reminders', models.ManyToManyField(to='tasks.reminder')), + ('tags', models.ManyToManyField(to='tasks.tag')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Subtask', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField()), + ('completed', models.BooleanField(default=False)), + ('parent_task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.task')), + ], + ), + ] diff --git a/backend/tasks/migrations/0002_alter_task_reminders_alter_task_tags.py b/backend/tasks/migrations/0002_alter_task_reminders_alter_task_tags.py new file mode 100644 index 0000000..4601c15 --- /dev/null +++ b/backend/tasks/migrations/0002_alter_task_reminders_alter_task_tags.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.6 on 2023-10-29 11:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='reminders', + field=models.ManyToManyField(blank=True, to='tasks.reminder'), + ), + migrations.AlterField( + model_name='task', + name='tags', + field=models.ManyToManyField(blank=True, to='tasks.tag'), + ), + ] diff --git a/backend/tasks/migrations/0003_alter_task_difficulty.py b/backend/tasks/migrations/0003_alter_task_difficulty.py new file mode 100644 index 0000000..55d1aac --- /dev/null +++ b/backend/tasks/migrations/0003_alter_task_difficulty.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-10-29 12:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0002_alter_task_reminders_alter_task_tags'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='difficulty', + field=models.PositiveSmallIntegerField(choices=[(1, 'Easy'), (2, 'Normal'), (3, 'Hard'), (4, 'Very Hard'), (5, 'Devil')]), + ), + ] diff --git a/backend/tasks/migrations/0004_rename_time_reminder_alerttime_and_more.py b/backend/tasks/migrations/0004_rename_time_reminder_alerttime_and_more.py new file mode 100644 index 0000000..0438ce9 --- /dev/null +++ b/backend/tasks/migrations/0004_rename_time_reminder_alerttime_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.6 on 2023-10-29 12:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0003_alter_task_difficulty'), + ] + + operations = [ + migrations.RenameField( + model_name='reminder', + old_name='time', + new_name='alertTime', + ), + migrations.AlterField( + model_name='reminder', + name='startDate', + field=models.DateField(auto_now_add=True, null=True), + ), + ] diff --git a/backend/tasks/migrations/__init__.py b/backend/tasks/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tasks/misc/serializers.py b/backend/tasks/misc/serializers.py new file mode 100644 index 0000000..99b312e --- /dev/null +++ b/backend/tasks/misc/serializers.py @@ -0,0 +1,12 @@ +from rest_framework import serializers +from ..models import Reminder, Tag + +class ReminderSerializer(serializers.ModelSerializer): + class Meta: + model = Reminder + fields = '__all__' + +class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + fields = '__all__' \ No newline at end of file diff --git a/backend/tasks/misc/views.py b/backend/tasks/misc/views.py new file mode 100644 index 0000000..e5feb01 --- /dev/null +++ b/backend/tasks/misc/views.py @@ -0,0 +1,11 @@ +from rest_framework import viewsets +from ..models import Reminder, Tag +from .serializers import ReminderSerializer, TagSerializer + +class ReminderViewSet(viewsets.ModelViewSet): + queryset = Reminder.objects.all() + serializer_class = ReminderSerializer + +class TagViewSet(viewsets.ModelViewSet): + queryset = Tag.objects.all() + serializer_class = TagSerializer \ No newline at end of file diff --git a/backend/tasks/models.py b/backend/tasks/models.py new file mode 100644 index 0000000..2f3f34a --- /dev/null +++ b/backend/tasks/models.py @@ -0,0 +1,157 @@ +from django.db import models +from django.conf import settings +from django.core import validators + + +class Reminder(models.Model): + """ + Represents a reminder associated with a task. + Fields: + - startDate: The optional date for which the reminder is set. + - time: The time at which the reminder is triggered. + """ + startDate = models.DateField(auto_now_add=True, null=True, blank=True) + alertTime = models.DateTimeField(null=False, blank=False) + +class Tag(models.Model): + """ + Represents a tag that can be associated with tasks. + Fields: + - name: The unique name of the tag. + """ + name = models.CharField(max_length=255) + +class Task(models.Model): + """ + Represents a task, such as Habit, Daily, Todo, or Reward. + Fields: + - type: The type of the tasks + - title: Title of the task. + - notes: Optional additional notes for the task. + - tags: Associated tags for the task. + - completed: A boolean field indicating whether the task is completed. + - exp: The experience values user will get from the task. + - priority: The priority of the task (range: 0.1 to 2). + - difficulty: The difficulty of the task (range: 1 to 5). + - attribute: The attribute linked to the task + - user: The user who owns the task. + - challenge: Associated challenge (optional). + - reminders: A Many-to-Many relationship with Reminder. + - fromSystem: A boolean field indicating if the task is from System. + - creation_date: Creation date of the task. + - last_update: Last updated date of the task. + """ + TASK_TYPES = [ + ('daily', 'Daily'), + ('habit', 'Habit'), + ('todo', 'Todo'), + ('Long Term Goal', 'Long Term Goal'), + ] + + DIFFICULTY_CHOICES = [ + (1, 'Easy'), + (2, 'Normal'), + (3, 'Hard'), + (4, 'Very Hard'), + (5, 'Devil'), + ] + + type = models.CharField(max_length=15, choices=TASK_TYPES, default='habit') + title = models.TextField() + notes = models.TextField(default='') + tags = models.ManyToManyField(Tag, blank=True) + completed = models.BooleanField(default=False) + exp = models.FloatField(default=0) + priority = models.FloatField(default=1, validators=[ + validators.MinValueValidator(0.1), + validators.MaxValueValidator(2), + ]) + difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES) + attribute = models.CharField(max_length=15, choices=[ + ('str', 'Strength'), + ('int', 'Intelligence'), + ('end', 'Endurance'), + ('per', 'Perception'), + ('luck', 'Luck'), + ], default='str') + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + challenge = models.BooleanField(default=False) + reminders = models.ManyToManyField(Reminder, blank=True) + fromSystem = models.BooleanField(default=False) + creation_date = models.DateTimeField(auto_now_add=True) + last_update = models.DateTimeField(auto_now=True) + + +class Subtask(models.Model): + """ + Represents a subtask associated with a task. + - description: Description of the subtask. + - completed: A boolean field indicating whether the subtask is completed. + - parent_task: The parent task of the subtask. + """ + description = models.TextField() + completed = models.BooleanField(default=False) + parent_task = models.ForeignKey(Task, on_delete=models.CASCADE) + + +class UserNotification(models.Model): + """ + Represents a user notification. + Fields: + - type: The type of the notification (e.g., 'NEW_CHAT_MESSAGE'). + - data: JSON data associated with the notification. + - seen: A boolean field indicating whether the notification has been seen. + """ + NOTIFICATION_TYPES = ( + ('LEVEL_UP', 'Level Up'), + ('DEATH', 'Death'), + ) + + type = models.CharField(max_length=255, choices=[type for type in NOTIFICATION_TYPES]) + data = models.JSONField(default=dict) + seen = models.BooleanField(default=False) + + @staticmethod + def clean_notification(notifications): + """ + Cleanup function for removing corrupt notification data: + - Removes notifications with null or missing id or type. + """ + if not notifications: + return notifications + + filtered_notifications = [] + + for notification in notifications: + if notification.id is None or notification.type is None: + continue + + return filtered_notifications + + +class Transaction(models.Model): + """ + Represents a transaction involving currencies in the system. + Fields: + - currency: The type of currency used in the transaction + - transactionType: The type of the transaction + - description: Additional text. + - amount: The transaction amount. + - user: The user involved in the transaction. + """ + CURRENCIES = (('gold', 'Gold'),) + TRANSACTION_TYPES = ( + ('buy_gold', 'Buy Gold'), + ('spend', 'Spend'), + ('debug', 'Debug'), + ('force_update_gold', 'Force Update Gold'), + ) + + currency = models.CharField(max_length=12, choices=CURRENCIES) + transaction_type = models.CharField(max_length=24, choices=TRANSACTION_TYPES) + description = models.TextField(blank=True) + amount = models.FloatField(default=0) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + + def __str__(self): + return f"Transaction ({self.id})" \ No newline at end of file diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py new file mode 100644 index 0000000..009c472 --- /dev/null +++ b/backend/tasks/tasks/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers +from ..models import Task + +class TaskCreateSerializer(serializers.ModelSerializer): + class Meta: + model = Task + # fields = '__all__' + exclude = ('tags', 'reminders') + + def create(self, validated_data): + # Create a new task with validated data + return Task.objects.create(**validated_data) + +class TaskGeneralSerializer(serializers.ModelSerializer): + class Meta: + model = Task + fields = '__all__' + + def create(self, validated_data): + # Create a new task with validated data + return Task.objects.create(**validated_data) \ No newline at end of file diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py new file mode 100644 index 0000000..0f75ced --- /dev/null +++ b/backend/tasks/tasks/views.py @@ -0,0 +1,37 @@ +from rest_framework import status +from rest_framework.response import Response +from rest_framework.generics import CreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, DestroyAPIView +from rest_framework.permissions import IsAuthenticated +from ..models import Task +from .serializers import TaskCreateSerializer, TaskGeneralSerializer + +class TaskCreateView(CreateAPIView): + queryset = Task.objects.all() + serializer_class = TaskCreateSerializer + permission_classes = [IsAuthenticated] + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + + if serializer.is_valid(): + self.perform_create(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class TaskRetrieveView(RetrieveAPIView): + queryset = Task.objects.all() + serializer_class = TaskGeneralSerializer + permission_classes = [IsAuthenticated] + + +class TaskUpdateView(RetrieveUpdateAPIView): + queryset = Task.objects.all() + serializer_class = TaskGeneralSerializer + permission_classes = [IsAuthenticated] + + +class TaskDeleteView(DestroyAPIView): + queryset = Task.objects.all() + permission_classes = [IsAuthenticated] \ No newline at end of file diff --git a/backend/tasks/tests/__init__.py b/backend/tasks/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tasks/tests/test_task_creation.py b/backend/tasks/tests/test_task_creation.py new file mode 100644 index 0000000..ff3c1ff --- /dev/null +++ b/backend/tasks/tests/test_task_creation.py @@ -0,0 +1,73 @@ +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase + +from .utils import create_test_user, login_user +from ..models import Task + +class TaskCreateViewTests(APITestCase): + def setUp(self): + + self.user = create_test_user() + self.client = login_user(self.user) + self.url = reverse("add-task") + + def test_create_valid_task(self): + """ + Test creating a valid task using the API. + """ + data = { + 'title': 'Test Task', + 'type': 'habit', + 'exp': 10, + 'attribute': 'str', + 'priority': 1.5, + 'difficulty': 1, + 'user': self.user.id, + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Task.objects.count(), 1) + self.assertEqual(Task.objects.get().title, 'Test Task') + + def test_create_invalid_task(self): + """ + Test creating an invalid task using the API. + """ + data = { + 'type': 'invalid', # Invalid task type + } + + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Task.objects.count(), 0) # No task should be created + + def test_missing_required_fields(self): + """ + Test creating a task with missing required fields using the API. + """ + data = { + 'title': 'Incomplete Task', + 'type': 'habit', + } + + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Task.objects.count(), 0) # No task should be created + + def test_invalid_user_id(self): + """ + Test creating a task with an invalid user ID using the API. + """ + data = { + 'title': 'Test Task', + 'type': 'habit', + 'exp': 10, + 'priority': 1.5, + 'difficulty': 1, + 'user': 999, # Invalid user ID + } + + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(Task.objects.count(), 0) # No task should be created diff --git a/backend/tasks/tests/utils.py b/backend/tasks/tests/utils.py new file mode 100644 index 0000000..80767fc --- /dev/null +++ b/backend/tasks/tests/utils.py @@ -0,0 +1,66 @@ +from rest_framework.test import APIClient + +from users.models import CustomUser +from ..models import Task + + +def create_test_user(email="testusertestuser@example.com", username="testusertestuser", + first_name="Test", password="testpassword",): + """create predifined user for testing""" + return CustomUser.objects.create_user( + email=email, + username=username, + first_name=first_name, + password=password, + ) + + +def login_user(user): + """Login a user to API client.""" + + client = APIClient() + client.force_authenticate(user=user) + return client + + +def create_task_json(user, **kwargs): + """Create task JSON data to use with the API.""" + defaults = { + "title": "Test Task", + "type": "habit", + "notes": "This is a test task created via the API.", + "exp": 10, + "priority": 1.5, + "difficulty": 1, + "attribute": "str", + "challenge": False, + "reminders": False, + "fromSystem": False, + "creation_date": None, + "last_update": None, + } + + task_attributes = {**defaults, **kwargs} + task_attributes["user"] = user + + return task_attributes + + +def create_test_task(user, **kwargs): + """Create a test task and associate it with the given user.""" + defaults = { + 'title': "Test Task", + 'task_type': 'habit', + 'notes': "This is a test task created via the API.", + 'exp': 10, + 'priority': 1.5, + 'difficulty': 1, + 'attribute': 'str', + 'challenge': False, + 'reminders': False, + 'fromSystem': False, + } + + task_attributes = {**defaults, **kwargs} + + return Task.objects.create(user=user, **task_attributes) \ No newline at end of file diff --git a/backend/tasks/urls.py b/backend/tasks/urls.py new file mode 100644 index 0000000..ef94e06 --- /dev/null +++ b/backend/tasks/urls.py @@ -0,0 +1,16 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .tasks.views import TaskCreateView, TaskRetrieveView, TaskUpdateView, TaskDeleteView +from .misc.views import TagViewSet, ReminderViewSet + +router = DefaultRouter() +router.register(r'reminders', ReminderViewSet) +router.register(r'tags', TagViewSet) + +urlpatterns = [ + path('', include(router.urls)), + path('tasks/create/', TaskCreateView.as_view(), name="add-task"), + path('tasks//', TaskRetrieveView.as_view(), name='retrieve-task'), + path('tasks//update/', TaskUpdateView.as_view(), name='update-task'), + path('tasks//delete/', TaskDeleteView.as_view(), name='delete-task'), +] \ No newline at end of file diff --git a/backend/users/models.py b/backend/users/models.py index 03d6e12..e473572 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -26,3 +26,17 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): def __str__(self): # String representation of the user return self.username + + +# class UserStats(models.Model): +# """ +# Represents User Profiles and Attributes. +# Fields: +# - health: health points of the user. +# - gold: gold points of the user. +# - experience: experience points of the user. +# """ +# user = models.OneToOneField(CustomUser, on_delete=models.CASCADE) +# health = models.IntegerField(default=100) +# gold = models.IntegerField(default=0) +# experience = models.FloatField(default=0) \ No newline at end of file