From 02fae1ba3fd363201c19d1a6cae9555989e4ed7d Mon Sep 17 00:00:00 2001 From: imbeer <76579340+imbeer@users.noreply.github.com> Date: Sun, 11 May 2025 17:00:58 +0300 Subject: [PATCH 01/52] #145: add auto-restart to containers * add auto-restart to containers * add dockerignore --- backend/.dockerignore | 6 ++++++ backend/docker-compose.yaml | 2 ++ 2 files changed, 8 insertions(+) create mode 100644 backend/.dockerignore diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..afc841dd --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.log +.env +venv/ +.git/ diff --git a/backend/docker-compose.yaml b/backend/docker-compose.yaml index 33491951..5c8ac3c8 100644 --- a/backend/docker-compose.yaml +++ b/backend/docker-compose.yaml @@ -1,6 +1,7 @@ services: database: image: postgres:17 + restart: always environment: POSTGRES_DB: ${DATABASE_NAME} POSTGRES_USER: ${DATABASE_USERNAME} @@ -21,6 +22,7 @@ services: # build: . image: taskbench/taskbench-backend:latest container_name: taskbench-backend + restart: always ports: - "8000:8000" depends_on: From 1b5fee904f296a0df7266cab760ca29fbeef05db Mon Sep 17 00:00:00 2001 From: imbeer <76579340+imbeer@users.noreply.github.com> Date: Mon, 19 May 2025 22:26:19 +0300 Subject: [PATCH 02/52] #83: refactor task and user views and services * remove auto restart docker-compose.override.yaml for local testing * move user logic to user_service.py and refactor * refactor get task list * refactor task creation * refactor task deletion (completion) * refactor task update * refactor category * refactor task, subtask and category views and services * add API for admin panel statistics --- backend/Script_creating_users.py | 4 +- backend/backend/urls.py | 8 +- backend/dashboard/urls.py | 2 +- backend/dashboard/views.py | 44 +- backend/docker-compose.override.yaml | 4 + .../migrations/0005_user_access_at.py | 19 + backend/taskbench/models/models.py | 1 + .../serializers/category_serializers.py | 34 + backend/taskbench/serializers/serializers.py | 13 - ...erializer.py => statistics_serializers.py} | 0 .../serializers/subtask_serializers.py | 13 + .../taskbench/serializers/task_serializers.py | 94 ++- .../taskbench/serializers/user_serializers.py | 33 +- .../taskbench/services/category_service.py | 24 + backend/taskbench/services/jwt_service.py | 14 - backend/taskbench/services/subtask_service.py | 46 ++ backend/taskbench/services/task_service.py | 130 ++++ backend/taskbench/services/user_service.py | 84 +++ backend/taskbench/tests/test_task_api.py | 6 +- backend/taskbench/utils/exceptions.py | 17 + backend/taskbench/views/category_views.py | 46 ++ backend/taskbench/views/statisctics_views.py | 6 +- backend/taskbench/views/subtask_views.py | 77 +++ backend/taskbench/views/suggestion_views.py | 4 +- backend/taskbench/views/task_views.py | 579 ++---------------- backend/taskbench/views/user_views.py | 129 ++-- backend/templates/dashboard/dashboard.html | 76 +-- 27 files changed, 788 insertions(+), 719 deletions(-) create mode 100644 backend/taskbench/migrations/0005_user_access_at.py create mode 100644 backend/taskbench/serializers/category_serializers.py rename backend/taskbench/serializers/{statistics_serializer.py => statistics_serializers.py} (100%) create mode 100644 backend/taskbench/serializers/subtask_serializers.py create mode 100644 backend/taskbench/services/category_service.py delete mode 100644 backend/taskbench/services/jwt_service.py create mode 100644 backend/taskbench/services/subtask_service.py create mode 100644 backend/taskbench/services/task_service.py create mode 100644 backend/taskbench/services/user_service.py create mode 100644 backend/taskbench/utils/exceptions.py create mode 100644 backend/taskbench/views/category_views.py create mode 100644 backend/taskbench/views/subtask_views.py diff --git a/backend/Script_creating_users.py b/backend/Script_creating_users.py index c7d99308..58d851d5 100644 --- a/backend/Script_creating_users.py +++ b/backend/Script_creating_users.py @@ -1,7 +1,7 @@ -import requests -import json from datetime import datetime, timedelta +import requests + BASE_URL = "http://127.0.0.1:8000" USERS = [ {"email": "user1@example.com", "password": "password1"}, diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 4c32f897..5c2dbf31 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -17,14 +17,16 @@ from django.contrib import admin from django.urls import path, include +from taskbench.views.category_views import CategoryListView from taskbench.views.statisctics_views import StatisticsView +from taskbench.views.subtask_views import ( + SubtaskCreateView, + SubtaskDetailView +) from taskbench.views.suggestion_views import SuggestionView from taskbench.views.task_views import ( TaskListView, TaskDetailView, - SubtaskCreateView, - SubtaskDetailView, - CategoryListView ) from taskbench.views.user_views import ( RegisterView, diff --git a/backend/dashboard/urls.py b/backend/dashboard/urls.py index 068c3229..9ebad351 100644 --- a/backend/dashboard/urls.py +++ b/backend/dashboard/urls.py @@ -5,6 +5,6 @@ path("admin/login/", custom_login, name="custom_login"), path("admin/dashboard/", dashboard_view, name="dashboard"), path("admin/subscriptions/", subscription_page, name="admin_subscriptions"), - path("admin/api/stats/", stats_api, name="stats_api"), + path("admin/dashboard/stats/", stats_api, name="stats_api"), path("admin/api/subscriptions/", subscription_list_api, name="subscription_list_api"), ] diff --git a/backend/dashboard/views.py b/backend/dashboard/views.py index ef7d52db..f0159da4 100644 --- a/backend/dashboard/views.py +++ b/backend/dashboard/views.py @@ -1,11 +1,13 @@ +from datetime import timedelta +from django.utils import timezone from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required, user_passes_test from django.core.paginator import Paginator -from django.shortcuts import render, redirect from django.http import HttpResponseForbidden, JsonResponse -from django.views.decorators.csrf import csrf_protect, csrf_exempt +from django.shortcuts import render, redirect +from django.views.decorators.csrf import csrf_protect -from taskbench.models.models import Subscription +from taskbench.models.models import Subscription, Subtask, Task, User @csrf_protect @@ -35,21 +37,27 @@ def subscription_page(request): @user_passes_test(lambda u: u.is_staff) def stats_api(request): - data = { - "total_users": 280, - "total_subscribers": 28, - "new_users_week": 40, - "new_users_today": 10, - "new_subs_week": 6, - "new_subs_today": 2, - "active_users_week": 144, - "active_users_today": 67, - "total_tasks": 20509, - "tasks_week": 1563, - "tasks_today": 250, - "total_subtasks": 48524, - } - return JsonResponse(data) + if request.method == "GET": + now = timezone.now() + today = now.date() + start_of_week = now - timedelta(days=now.weekday()) + start_of_week = start_of_week.replace(hour=0, minute=0, second=0, microsecond=0) + + data = { + "total_users": User.objects.all().count(), + "total_subscribers": Subscription.objects.values('user').distinct().count(), + "new_users_week": User.objects.filter(created_at__gte=start_of_week).count(), + "new_users_today": User.objects.filter(created_at__date=today).count(), + "new_subs_week": Subscription.objects.filter(start_date__gt=start_of_week).count(), + "new_subs_today": Subscription.objects.filter(start_date__date=today).count(), + "active_users_week": User.objects.filter(access_at__gt=start_of_week).count(), + "active_users_today": User.objects.filter(access_at__date=today).count(), + "total_tasks": Task.objects.all().count(), + "tasks_week": Task.objects.filter(created_at__gte=start_of_week).count(), + "tasks_today": Task.objects.filter(created_at__date=today).count(), + "total_subtasks": Subtask.objects.all().count(), + } + return JsonResponse(data) @user_passes_test(lambda u: u.is_staff) def subscription_list_api(request): diff --git a/backend/docker-compose.override.yaml b/backend/docker-compose.override.yaml index dfd764ad..5018956e 100644 --- a/backend/docker-compose.override.yaml +++ b/backend/docker-compose.override.yaml @@ -2,6 +2,10 @@ services: taskbench-backend: build: . image: taskbench/taskbench-backend:dev + restart: no + + database: + restart: no # test: # image: alpine:latest diff --git a/backend/taskbench/migrations/0005_user_access_at.py b/backend/taskbench/migrations/0005_user_access_at.py new file mode 100644 index 00000000..04029d24 --- /dev/null +++ b/backend/taskbench/migrations/0005_user_access_at.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2 on 2025-05-19 18:38 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('taskbench', '0004_task_completed_at'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='access_at', + field=models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + ] diff --git a/backend/taskbench/models/models.py b/backend/taskbench/models/models.py index cd392b98..f74a10ef 100644 --- a/backend/taskbench/models/models.py +++ b/backend/taskbench/models/models.py @@ -8,6 +8,7 @@ class User(models.Model): email = models.EmailField(max_length=100, unique=True, null=False) password_hash = models.CharField(max_length=255, null=False) created_at = models.DateTimeField(default=timezone.now) + access_at = models.DateTimeField(default=timezone.now, null=True) def __str__(self): return self.email diff --git a/backend/taskbench/serializers/category_serializers.py b/backend/taskbench/serializers/category_serializers.py new file mode 100644 index 00000000..32cf0cef --- /dev/null +++ b/backend/taskbench/serializers/category_serializers.py @@ -0,0 +1,34 @@ +from django.http import JsonResponse + +from taskbench.models.models import Category +from rest_framework import serializers, status + + +def category_list_response(categories, status): + data = [{ + "id": category.category_id, + "name": category.name + } for category in categories] + + return JsonResponse(data, safe=False, status=status) + + +def category_response(category, status): + return JsonResponse(category_json(category), safe=False, status=status) + + +def category_json(category: Category): + return { + "id": category.category_id, + "name": category.name + } + + +class CategorySerializer(serializers.Serializer): + name = serializers.CharField(required=True) + + def validate(self, data): + category_name = data.get("name") + if len(category_name) > 50: + raise serializers.ValidationError("Category name too long (max 50 chars)") + return {"name": category_name} diff --git a/backend/taskbench/serializers/serializers.py b/backend/taskbench/serializers/serializers.py index 0344dd8f..92b8cf90 100644 --- a/backend/taskbench/serializers/serializers.py +++ b/backend/taskbench/serializers/serializers.py @@ -1,12 +1,5 @@ from rest_framework import serializers from ..models.models import User, Task, Subtask, Category, TaskCategory - -class UserSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = '__all__' - - class TaskSerializer(serializers.ModelSerializer): class Meta: model = Task @@ -19,12 +12,6 @@ class Meta: fields = '__all__' -class CategorySerializer(serializers.ModelSerializer): - class Meta: - model = Category - fields = '__all__' - - class TaskCategorySerializer(serializers.ModelSerializer): class Meta: model = TaskCategory diff --git a/backend/taskbench/serializers/statistics_serializer.py b/backend/taskbench/serializers/statistics_serializers.py similarity index 100% rename from backend/taskbench/serializers/statistics_serializer.py rename to backend/taskbench/serializers/statistics_serializers.py diff --git a/backend/taskbench/serializers/subtask_serializers.py b/backend/taskbench/serializers/subtask_serializers.py new file mode 100644 index 00000000..7bfe6925 --- /dev/null +++ b/backend/taskbench/serializers/subtask_serializers.py @@ -0,0 +1,13 @@ +from django.http import JsonResponse + + +def subtask_json(subtask): + return { + "id": subtask.subtask_id, + "content": subtask.text, + "is_done": subtask.is_completed + } + + +def subtask_response(subtask, status): + return JsonResponse(subtask_json(subtask), status=status) diff --git a/backend/taskbench/serializers/task_serializers.py b/backend/taskbench/serializers/task_serializers.py index d74ea4b8..13b1c765 100644 --- a/backend/taskbench/serializers/task_serializers.py +++ b/backend/taskbench/serializers/task_serializers.py @@ -1,4 +1,55 @@ +from enum import Enum + +from django.http import JsonResponse from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from taskbench.serializers.subtask_serializers import subtask_json + + +class Sort(Enum): + PRIORITY = 'priority' + DEADLINE = 'deadline' + DEFAULT = '' + + +def match_sort(sort_by: str | None) -> Sort: + if sort_by is None: + return Sort.DEFAULT + for sort in Sort: + if sort.value == sort_by: + return sort + return Sort.DEFAULT + + +def task_list_response(tasks): + data = [] + for task in tasks: + category = task.task_categories.first().category if task.task_categories.first() else None + data.append(task_json(task, category, task.subtasks.all())) + return JsonResponse(data, safe=False) + + +def task_response(task, status): + category = task.task_categories.first().category if task.task_categories.first() else None + return JsonResponse(task_json(task, category, task.subtasks.all()), safe=False, status=status) + + +def task_json(task, category, subtasks): + return { + "id": task.task_id, + "content": task.title, + "is_done": False, + "dpc": { + "deadline": task.deadline.replace(tzinfo=None).isoformat( + timespec='seconds') if task.deadline is not None else None, + "priority": task.priority, + "category_id": category.category_id if category else 0, + "category_name": category.name if category else "" + }, + "subtasks": [subtask_json(subtask) for subtask in subtasks] + } + class TaskDPCtoFlatSerializer(serializers.Serializer): category_id = serializers.IntegerField(required=False, allow_null=True) @@ -8,11 +59,48 @@ class TaskDPCtoFlatSerializer(serializers.Serializer): timestamp = serializers.DateTimeField() def to_internal_value(self, data): - # Flatten 'dpc' into the main data dpc_data = data.get('dpc', {}) if dpc_data: - # Merge dpc fields into main data data = {**dpc_data, **data} - data.pop('dpc', None) # Remove 'dpc' after merging + data.pop('dpc', None) return super().to_internal_value(data) + + +class TaskSearchParametersSerializer(serializers.Serializer): + category_id = serializers.IntegerField(required=False, allow_null=True) + sort_by = serializers.CharField(required=False, allow_null=True, allow_blank=True) + after = serializers.DateTimeField(required=False, allow_null=True) + before = serializers.DateTimeField(required=False, allow_null=True) + date = serializers.DateTimeField(required=False, allow_null=True) + offset = serializers.IntegerField(required=False, allow_null=True, default=0) + limit = serializers.IntegerField(required=False, allow_null=True, default=10) + + def validate(self, data): + validated_data = {'sort_by': match_sort(data.get('sort_by'))} + + date = data.get('date') + before = data.get('before') + after = data.get('after') + + validated_data['before'] = data.get('before') + validated_data['after'] = data.get('after') + + if date is not None: + if (after is not None) or (before is not None): + raise ValidationError('Conflicting date parameters') + validated_data['before'] = None + validated_data['after'] = None + validated_data['date'] = date + else: + validated_data['before'] = data.get('before') + validated_data['after'] = data.get('after') + validated_data['date'] = None + if (before is not None) and (after is not None) and (before < after): + raise ValidationError('Invalid date') + + validated_data['offset'] = data.get('offset') if data.get('offset') is not None else 0 + validated_data['limit'] = data.get('limit') if data.get('limit') is not None else 10 + validated_data['category_id'] = data.get('category_id') + + return validated_data diff --git a/backend/taskbench/serializers/user_serializers.py b/backend/taskbench/serializers/user_serializers.py index f2c4aa84..e737ff3e 100644 --- a/backend/taskbench/serializers/user_serializers.py +++ b/backend/taskbench/serializers/user_serializers.py @@ -1,27 +1,34 @@ from datetime import datetime -from rest_framework.exceptions import ValidationError +from django.http import JsonResponse from rest_framework import serializers +from rest_framework.exceptions import ValidationError from rest_framework_simplejwt.tokens import UntypedToken from backend import settings from ..models.models import User +def user_response(user, refresh: str, access: str, status): + return JsonResponse({ + 'user_id': user.user_id, + 'access': access, + 'refresh': refresh, + }, status=status) -class UserRegisterSerializer(serializers.ModelSerializer): +class UserRegisterSerializer(serializers.Serializer): + email = serializers.EmailField() password = serializers.CharField(write_only=True) - class Meta: - model = User - fields = ['email', 'password'] - - def create(self, validated_data): - password = validated_data.pop('password') - user = User.objects.create(**validated_data) - user.set_password(password) - user.username = user.email - user.save() - return user + def validate(self, data): + email = data['email'] + password = data['password'] + if (email is None) or (password is None): + raise ValidationError('Both email and password are required') + if User.objects.filter(email=email).exists(): + raise ValidationError('Email already registered') + if len(password) < 8: + raise ValidationError('Password must be at least 8 characters') + return data class LoginSerializer(serializers.Serializer): email = serializers.EmailField() diff --git a/backend/taskbench/services/category_service.py b/backend/taskbench/services/category_service.py new file mode 100644 index 00000000..c53f6b53 --- /dev/null +++ b/backend/taskbench/services/category_service.py @@ -0,0 +1,24 @@ +from rest_framework.exceptions import ValidationError + +from taskbench.models.models import Category +from taskbench.serializers.category_serializers import CategorySerializer +from taskbench.services.user_service import get_user +from taskbench.utils.exceptions import AlreadyExists + + +def get_category_list(token): + user = get_user(token) + return Category.objects.filter(user=user) + +def create_category(token, data): + serializer = CategorySerializer(data=data) + user = get_user(token) + if serializer.is_valid(): + category_name = serializer.validated_data["name"] + if Category.objects.filter(name=category_name, user=user).exists(): + raise AlreadyExists("Category already exists") + category = Category.objects.create(name=category_name, user=user) + return category + else: + raise ValidationError(serializer.errors) + diff --git a/backend/taskbench/services/jwt_service.py b/backend/taskbench/services/jwt_service.py deleted file mode 100644 index 25025d21..00000000 --- a/backend/taskbench/services/jwt_service.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.http import HttpRequest -from rest_framework_simplejwt.exceptions import AuthenticationFailed, TokenError -from rest_framework_simplejwt.tokens import RefreshToken, UntypedToken -from rest_framework_simplejwt.settings import api_settings - -from backend import settings -from taskbench.models.models import User -from taskbench.serializers.serializers import UserSerializer - - -def get_token_from_request(request: HttpRequest): - auth_header = request.headers.get('Authorization', '') - token = auth_header.split(' ')[1] if auth_header.startswith('Bearer ') else None - return {'token': token} \ No newline at end of file diff --git a/backend/taskbench/services/subtask_service.py b/backend/taskbench/services/subtask_service.py new file mode 100644 index 00000000..62d424a5 --- /dev/null +++ b/backend/taskbench/services/subtask_service.py @@ -0,0 +1,46 @@ +from rest_framework.exceptions import ValidationError + +from taskbench.models.models import Subtask +from taskbench.services.task_service import get_task +from taskbench.services.user_service import get_user +from taskbench.utils.exceptions import NotFound + + +def get_subtask(subtask_id, user): + try: + return Subtask.objects.get(subtask_id=subtask_id, task__user=user) + except Subtask.DoesNotExist: + raise NotFound("Subtask not found") + +def create_subtask(token, task_id, data): + user = get_user(token) + task = get_task(user=user, task_id=task_id) + + content = data.get('content') + is_done = data.get('is_done', False) + + if not content: + raise ValidationError('Content cannot be empty') + + return Subtask.objects.create( + text=content, + task=task, + is_completed=is_done + ) + +def update_subtask(subtask_id, token, data): + user = get_user(token) + subtask = get_subtask(subtask_id, user) + + if 'content' in data: + subtask.text = data['content'] + if 'is_done' in data: + subtask.is_completed = data['is_done'] + + subtask.save() + return subtask + +def delete_subtask(token, subtask_id): + user = get_user(token) + subtask = get_subtask(subtask_id, user) + subtask.delete() \ No newline at end of file diff --git a/backend/taskbench/services/task_service.py b/backend/taskbench/services/task_service.py new file mode 100644 index 00000000..7bdeb487 --- /dev/null +++ b/backend/taskbench/services/task_service.py @@ -0,0 +1,130 @@ +from django.utils.dateparse import parse_datetime +from rest_framework.exceptions import ValidationError + +from taskbench.models.models import Task, Category, TaskCategory, Subtask +from taskbench.serializers.task_serializers import TaskSearchParametersSerializer, Sort +from taskbench.services.user_service import get_user +from taskbench.utils.exceptions import NotFound + + +def get_task(user, task_id) -> Task: + try: + return Task.objects.get(task_id=task_id, user=user) + except Task.DoesNotExist: + raise NotFound("Task does not exist") + + +def get_category(user, category_id): + try: + return Category.objects.get(category_id=category_id, user=user) + except Category.DoesNotExist: + raise ValidationError('Category not found or access denied') + + +def get_task_list(token, params): + params_serializer = TaskSearchParametersSerializer(data=params) + + if not params_serializer.is_valid(): + raise ValidationError('Invalid params') + + user = get_user(token) + params = params_serializer.validated_data + + sort_by = params['sort_by'] + limit = params['limit'] + offset = params['offset'] + filters = { + 'user': user, + 'is_completed': False, + 'task_categories__category_id': params['category_id'], + 'deadline__gte': params['after'], + 'deadline__lte': params['before'], + 'deadline__date': params['date'] + } + filters = {k: v for k, v in filters.items() if v is not None} + + tasks = Task.objects.filter(**filters).prefetch_related('subtasks', 'task_categories__category') + + if sort_by == Sort.PRIORITY or sort_by == Sort.DEFAULT: + tasks = tasks.order_by('-priority', 'deadline', 'task_id') + elif sort_by == Sort.DEADLINE: + tasks = tasks.order_by('deadline', '-priority', 'task_id') + + tasks = tasks[offset:offset + limit] + + return tasks + + +def create_task(token, data): + user = get_user(token) + content = data.get('content') + dpc = data.get('dpc', {}) + subtasks = data.get('subtasks', []) + + if not content: + raise ValidationError('Missing required field: content') + + task = Task.objects.create( + title=content, + deadline=parse_datetime(dpc.get('deadline')) if dpc.get('deadline') else None, + priority=dpc.get('priority', 0), + user=user, + is_completed=False + ) + if 'category_id' in dpc: + category = get_category(user=user, category_id=dpc['category_id']) + TaskCategory.objects.create(task=task, category=category) + + for subtask_data in subtasks: + Subtask.objects.create( + text=subtask_data['content'], + task=task, + is_completed=False + ) + + return task + + +def complete_task(token, task_id): + user = get_user(token) + task = get_task(user=user, task_id=task_id) + if task.is_completed: + raise ValidationError('Task already completed') + task.is_completed = True + task.save() + return task + + +def update_task(token, task_id, data): + user = get_user(token) + task = get_task(user=user, task_id=task_id) + + if task is None: + raise NotFound("Task does not exist") + + content = data.get('content') + dpc = data.get('dpc', {}) + + if content: + task.title = content + if not dpc: + task.save() + return task + + if 'deadline' in dpc: + task.deadline = parse_datetime(dpc['deadline']) if dpc['deadline'] else None + if 'priority' in dpc: + try: + task.priority = int(dpc['priority']) + except: + raise ValidationError('Invalid priority') + if 'category_id' in dpc: + try: + category = get_category(user=user, category_id=dpc['category_id']) + TaskCategory.objects.filter(task=task, category=category).delete() + # task.task_categories.all().delete() # предыдущие категории + TaskCategory.objects.create(task=task, category=category) + except Category.DoesNotExist: + raise ValidationError('Category not found or access denied') + task.save() + return task diff --git a/backend/taskbench/services/user_service.py b/backend/taskbench/services/user_service.py new file mode 100644 index 00000000..cdce756d --- /dev/null +++ b/backend/taskbench/services/user_service.py @@ -0,0 +1,84 @@ +from django.http import HttpRequest +from django.utils import timezone +from rest_framework.exceptions import ValidationError +from rest_framework_simplejwt.tokens import RefreshToken + +from taskbench.models.models import User +from taskbench.serializers.user_serializers import JwtSerializer, UserRegisterSerializer, LoginSerializer +from taskbench.utils.exceptions import AuthenticationError + + +def get_token(request: HttpRequest): + auth_header = request.headers.get('Authorization', '') + token = auth_header.split(' ')[1] if auth_header.startswith('Bearer ') else None + return {'token': token} + + +def get_user(token): + # token = get_token(request) + serializer = JwtSerializer(data=token) + if serializer.is_valid(): + user = serializer.validated_data['user'] + user.access_at = timezone.now() + user.save() + return user + else: + raise AuthenticationError(str(serializer.errors)) + + +def generate_token(user): + refresh = RefreshToken.for_user(user) + access = refresh.access_token + return str(refresh), str(access) + + +def register_user(data): + serializer = UserRegisterSerializer(data=data) + if serializer.is_valid(): + password = serializer.validated_data.pop('password') + user = User(**serializer.validated_data) + user.set_password(password) + user.username = user.email + user.save() + refresh, access = generate_token(user) + return user, refresh, access + else: + raise ValidationError(serializer.errors) + + +def login_user(data): + serializer = LoginSerializer(data=data) + if serializer.is_valid(): + user = serializer.validated_data['user'] + refresh, access = generate_token(user) + return user, refresh, access + else: + raise ValidationError(serializer.errors) + + +def token_refresh(token): + user = get_user(token) + refresh, access = generate_token(user) + return user, refresh, access + + +def delete_user(token): + user = get_user(token) + user.delete() + + +def change_password(token, data): + user = get_user(token) + old_password = data.get('old_password') + new_password = data.get('new_password') + if not old_password or not new_password: + raise ValidationError('Both old_password and new_password are required') + + if not user.check_password(old_password): + raise ValidationError('Password is incorrect') + + try: + user.set_password(new_password) + user.save() + except Exception as e: + raise ValidationError(str(e)) diff --git a/backend/taskbench/tests/test_task_api.py b/backend/taskbench/tests/test_task_api.py index 073ab16d..a00cbf6c 100644 --- a/backend/taskbench/tests/test_task_api.py +++ b/backend/taskbench/tests/test_task_api.py @@ -148,8 +148,7 @@ def test_create_task(self): def test_invalid_date_filter(self): url = f"{reverse('task_list')}?date=invalid-date" response = self.client.get(url, **self.get_auth_headers()) - # Либо ожидаем 500 (если это текущее поведение), либо изменяем view чтобы возвращал 400 - self.assertEqual(response.status_code, 500) # или 400, в зависимости от того, что возвращает ваш API + self.assertEqual(response.status_code, 400) def test_update_task(self): url = reverse('task_detail', args=[self.task1.task_id]) @@ -224,8 +223,7 @@ def test_unauthorized_access(self): def test_invalid_date_filter(self): url = f"{reverse('task_list')}?date=invalid-date" response = self.client.get(url, **self.get_auth_headers()) - # Поскольку view возвращает 500 при невалидной дате, меняем ожидание - self.assertEqual(response.status_code, 500) + self.assertEqual(response.status_code, 400) self.assertIn('error', response.json()) def test_create_task_invalid_data(self): diff --git a/backend/taskbench/utils/exceptions.py b/backend/taskbench/utils/exceptions.py new file mode 100644 index 00000000..6c02f619 --- /dev/null +++ b/backend/taskbench/utils/exceptions.py @@ -0,0 +1,17 @@ +class BaseError(Exception): + def __init__(self, message): + self.message = message + super().__init__(self.message) + + def __str__(self): + return self.message + + +class AuthenticationError(BaseError): + pass + +class NotFound(BaseError): + pass + +class AlreadyExists(BaseError): + pass \ No newline at end of file diff --git a/backend/taskbench/views/category_views.py b/backend/taskbench/views/category_views.py new file mode 100644 index 00000000..6f7d762e --- /dev/null +++ b/backend/taskbench/views/category_views.py @@ -0,0 +1,46 @@ +from django.http import JsonResponse +from rest_framework.exceptions import ValidationError +import json +from rest_framework.views import APIView + +from taskbench.serializers.category_serializers import category_list_response, category_response +from taskbench.services.category_service import get_category_list, create_category +from taskbench.services.user_service import get_token +from taskbench.utils.exceptions import AuthenticationError, AlreadyExists + + +class CategoryListView(APIView): + """ + GET, POST http://127.0.0.1:8000/categories/ + """ + + def get(self, request, *args, **kwargs): + try: + token = get_token(request) + return category_list_response(get_category_list(token), 200) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + def post(self, request, *args, **kwargs): + """ + POST + { "name": "Хехе" } + """ + try: + token = get_token(request) + data = json.loads(request.body) + return category_response(create_category(token=token, data=data), 201) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except AlreadyExists as e: + return JsonResponse({'error': str(e)}, status=409) + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON'}, status=400) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) diff --git a/backend/taskbench/views/statisctics_views.py b/backend/taskbench/views/statisctics_views.py index 2e21d96d..8eeab25f 100644 --- a/backend/taskbench/views/statisctics_views.py +++ b/backend/taskbench/views/statisctics_views.py @@ -6,9 +6,9 @@ from rest_framework.response import Response from ..models.models import Task -from ..serializers.statistics_serializer import StatisticsSerializer +from ..serializers.statistics_serializers import StatisticsSerializer from ..serializers.user_serializers import JwtSerializer -from ..services.jwt_service import get_token_from_request +from ..services.user_service import get_token class StatisticsView(APIView): @@ -24,7 +24,7 @@ class StatisticsView(APIView): разделенное на максимальное количество выполненных задач за день в текущей неделе. """ def get(self, request, *args, **kwargs): - serializer = JwtSerializer(data=get_token_from_request(request)) + serializer = JwtSerializer(data=get_token(request)) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED) diff --git a/backend/taskbench/views/subtask_views.py b/backend/taskbench/views/subtask_views.py new file mode 100644 index 00000000..37a3105b --- /dev/null +++ b/backend/taskbench/views/subtask_views.py @@ -0,0 +1,77 @@ +import json +from django.http import JsonResponse, HttpResponse +from rest_framework.exceptions import ValidationError +from rest_framework.views import APIView + +from taskbench.serializers.subtask_serializers import subtask_response +from taskbench.services.subtask_service import create_subtask, update_subtask, delete_subtask +from taskbench.services.user_service import get_token +from taskbench.utils.exceptions import NotFound, AuthenticationError + + + +class SubtaskCreateView(APIView): + def post(self, request, *args, **kwargs): + """ + POST /subtasks + http://127.0.0.1:8000/subtasks/?task_id=3 + { "content": "Новая подзадача2", "is_done": false } + """ + try: + task_id = request.GET.get('task_id') + data = json.loads(request.body) + if not task_id: + return JsonResponse({'error': 'task_id parameter is required'}, status=400) + return subtask_response(create_subtask(token=get_token(request), task_id=task_id, data=data), status=201) + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON'}, status=400) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except NotFound as e: + return JsonResponse({'error': str(e)}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + + +class SubtaskDetailView(APIView): + def patch(self, request, subtask_id, *args, **kwargs): + """ + PATCH /subtasks/{subtask_id} + http://127.0.0.1:8000/subtasks/4/ + { "content": "Обновленный текст подзадачи", "is_done": true } + """ + try: + token = get_token(request) + data = json.loads(request.body) + return subtask_response(update_subtask(token=token, subtask_id=subtask_id, data=data), 200) + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON'}, status=400) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except NotFound as e: + return JsonResponse({'error': str(e)}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + def delete(self, request, subtask_id, *args, **kwargs): + """ + DELETE /subtasks/{subtask_id} + http://127.0.0.1:8000/subtasks/4/ + """ + try: + token = get_token(request) + delete_subtask(token=token, subtask_id=subtask_id) + return HttpResponse('deleted', status=204) + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except NotFound as e: + return JsonResponse({'error': str(e)}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) diff --git a/backend/taskbench/views/suggestion_views.py b/backend/taskbench/views/suggestion_views.py index 50d96e96..2beac158 100644 --- a/backend/taskbench/views/suggestion_views.py +++ b/backend/taskbench/views/suggestion_views.py @@ -10,7 +10,7 @@ from taskbench.models.models import Category from taskbench.serializers.task_serializers import TaskDPCtoFlatSerializer from taskbench.serializers.user_serializers import JwtSerializer -from taskbench.services.jwt_service import get_token_from_request +from taskbench.services.user_service import get_token # from taskbench.serializers.task_serializers import TaskSerializer from taskbench.services.suggestion_service import SuggestionService @@ -24,7 +24,7 @@ def post(self, request): serializer = TaskDPCtoFlatSerializer(data=data) if not serializer.is_valid(): return JsonResponse(serializer.errors, status=400) - user_serializer = JwtSerializer(data=get_token_from_request(request)) + user_serializer = JwtSerializer(data=get_token(request)) if not user_serializer.is_valid(): return Response("Invalid token", status=401) user_id = user_serializer.validated_data['user'].user_id diff --git a/backend/taskbench/views/task_views.py b/backend/taskbench/views/task_views.py index 193f4685..91a92b4c 100644 --- a/backend/taskbench/views/task_views.py +++ b/backend/taskbench/views/task_views.py @@ -1,542 +1,93 @@ -from django.shortcuts import render -from django.http import JsonResponse, HttpResponseNotAllowed, HttpResponseBadRequest, HttpResponse -from django.views.decorators.csrf import csrf_exempt -from django.utils.dateparse import parse_datetime -from rest_framework.views import APIView - -from ..models.models import Task, Subtask, TaskCategory, Category import json -from ..serializers.user_serializers import JwtSerializer -from ..services.jwt_service import get_token_from_request +from django.http import JsonResponse +from rest_framework.exceptions import ValidationError +from rest_framework.views import APIView + +from ..serializers.task_serializers import task_list_response, task_response +from ..services.task_service import get_task_list, create_task, complete_task, update_task +from ..services.user_service import get_token, AuthenticationError -# /tasks - GET, POST -#пример: GET http://127.0.0.1:8000/tasks/ -#GET http://127.0.0.1:8000/tasks/?sort_by=priority -#GET http://127.0.0.1:8000/tasks/?sort_by=deadline -#GET http://127.0.0.1:8000/tasks/?date=2026-05-25 - с фильтром по дате -# GET http://127.0.0.1:8000/tasks/?after=2025-01-01T00:00:00Z -# GET http://127.0.0.1:8000/tasks/?before=2025-12-31T23:59:59Z -# GET http://127.0.0.1:8000/tasks/?after=2025-01-01T00:00:00Z&before=2025-12-31T23:59:59Z -# GET http://127.0.0.1:8000/tasks/?date=2025-05-01 -#POST http://127.0.0.1:8000/tasks/ -# { -# "content": "Подготовить презентацию", -# "dpc": { -# "deadline": "2025-05-25T14:00:00Z", -# "priority": 2, -# "category_id": 3 -# }, -# "subtasks": [ -# { -# "content": "Собрать материалы" -# }, -# { -# "content": "Создать черновик" -# } -# ] -# } class TaskListView(APIView): + """ + /tasks - GET, POST + """ def get(self, request, *args, **kwargs): + """ + GET http://127.0.0.1:8000/tasks/ + GET http://127.0.0.1:8000/tasks/?sort_by=priority + GET http://127.0.0.1:8000/tasks/?sort_by=deadline + GET http://127.0.0.1:8000/tasks/?date=2026-05-25 - с фильтром по дате + GET http://127.0.0.1:8000/tasks/?after=2025-01-01T00:00:00Z + GET http://127.0.0.1:8000/tasks/?before=2025-12-31T23:59:59Z + GET http://127.0.0.1:8000/tasks/?after=2025-01-01T00:00:00Z&before=2025-12-31T23:59:59Z + GET http://127.0.0.1:8000/tasks/?date=2025-05-01 + """ try: - # Get token from request and validate user - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse({'error': 'Invalid token'}, status=401) - user = serializer.validated_data['user'] - - # Get request parameters - category_id = request.GET.get('category_id') - sort_by = request.GET.get('sort_by') - after = request.GET.get('after') - before = request.GET.get('before') - date = request.GET.get('date') - offset = int(request.GET.get('offset', 0)) - limit = int(request.GET.get('limit', 10)) - - # Base query - exclude completed tasks - tasks = Task.objects.filter(user=user, is_completed=False) \ - .prefetch_related('subtasks', 'task_categories__category') - - # Validate filters - if date is not None and (after is not None or before is not None ): - return JsonResponse( - {'error': 'Use either date or after/before, not together'}, - status=400 - ) - - # Apply filters - if category_id is not None : - tasks = tasks.filter(task_categories__category_id=category_id) - - if date is not None : - tasks = tasks.filter(deadline__date=date) - - # Фильтрация по времени - if after is not None or before is not None : - # Исключаем задачи без дедлайна - tasks = tasks.exclude(deadline__isnull=True) - - if after is not None : - after_dt = parse_datetime(after) - if not after_dt: - return JsonResponse({'error': 'Invalid after datetime'}, status=400) - tasks = tasks.filter(deadline__gte=after_dt) - - if before is not None : - before_dt = parse_datetime(before) - if not before_dt: - return JsonResponse({'error': 'Invalid before datetime'}, status=400) - tasks = tasks.filter(deadline__lte=before_dt) - - # Apply sorting - if sort_by == 'priority': - tasks = tasks.order_by('-priority', 'deadline', 'task_id') # Вариант 2 - elif sort_by == 'deadline': - tasks = tasks.order_by('deadline', '-priority', 'task_id') # Вариант 1 - else: - tasks = tasks.order_by('-priority', 'deadline', 'task_id') # Дефолтный вариант - - # Pagination - tasks = tasks[offset:offset + limit] - - # Prepare response - data = [] - for task in tasks: - category = task.task_categories.first().category if task.task_categories.first() else None - - task_data = { - "id": task.task_id, - "content": task.title, - "is_done": False, - "dpc": { - "deadline": task.deadline.replace(tzinfo=None).isoformat(timespec='seconds') if task.deadline is not None else None, - "priority": task.priority, - "category_id": category.category_id if category else 0, - "category_name": category.name if category else "" - }, - "subtasks": [ - { - "id": subtask.subtask_id, - "content": subtask.text, - "is_done": subtask.is_completed - } - for subtask in task.subtasks.all() - ] - } - data.append(task_data) - - return JsonResponse(data, safe=False) - + token = get_token(request) + params = request.GET + return task_list_response(get_task_list(token=token, params=params)) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) except Exception as e: return JsonResponse({'error': str(e)}, status=500) def post(self, request, *args, **kwargs): + """ + POST http://127.0.0.1:8000/tasks/ + { + "content": "Подготовить презентацию", + "dpc": { "deadline": "2025-05-25T14:00:00Z", "priority": 2, "category_id": 3 }, + "subtasks": [{ "content": "Собрать материалы" }, { "content": "Создать черновик" }] + } + """ try: - # Get token from request and validate user - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse({'error': 'Invalid token'}, status=401) - user = serializer.validated_data['user'] - + token = get_token(request) data = json.loads(request.body) - task_text = data.get('content') - dpc = data.get('dpc', {}) - subtasks = data.get('subtasks', []) - - if not task_text: - return JsonResponse({'error': 'Missing required field: content'}, status=400) - - # Create task - task = Task.objects.create( - title=task_text, - deadline=parse_datetime(dpc.get('deadline')) if dpc.get('deadline') else None, - priority=dpc.get('priority', 0), - user=user, - is_completed=False - ) - - # Add category if specified - if 'category_id' in dpc: - category_id = dpc['category_id'] - try: - # Проверяем что категория принадлежит текущему пользователю - category = Category.objects.get(category_id=category_id, user=user) - except Category.DoesNotExist: - return JsonResponse( - {'error': 'Category not found or access denied'}, - status=400 - ) - - TaskCategory.objects.create(task=task, category=category) - - # Create subtasks - for subtask_data in subtasks: - Subtask.objects.create( - text=subtask_data['content'], - task=task, - is_completed=False - ) - - # Get updated task data - category = task.task_categories.first().category if task.task_categories.first() else None - subtasks_data = [{ - "id": s.subtask_id, - "content": s.text, - "is_done": s.is_completed - } for s in task.subtasks.all()] - - response_data = { - "id": task.task_id, - "content": task.title, - "is_done": False, - "dpc": { - "deadline": task.deadline.replace(tzinfo=None).isoformat(timespec='seconds') if task.deadline else None, - "priority": task.priority, - "category_id": category.category_id if category else 0, - "category_name": category.name if category else "" - }, - "subtasks": subtasks_data - } - - return JsonResponse(response_data, status=201) - + return task_response(create_task(token=token, data=data), 201) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) - except Exception as e: - return JsonResponse({'error': str(e)}, status=400) - - -# /tasks/{task_id} - PATCH, DELETE -#пример:DELETE http://127.0.0.1:8000/tasks/2/ -#PATCH http://127.0.0.1:8000/tasks/1/ -# { -# "content": "Подготовить отчетфыфыфыф", -# "dpc": { -# "deadline": "2025-04-30T18:00:00Z", -# "priority": 3, -# "category_id": 5 -# } -# } class TaskDetailView(APIView): - def get_task(self, task_id, user): - try: - return Task.objects.get(task_id=task_id, user=user) - except Task.DoesNotExist: - return None - + """ + /tasks/{task_id} - PATCH, DELETE + """ def delete(self, request, task_id, *args, **kwargs): - # Get token from request and validate user - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse({'error': 'Invalid token'}, status=401) - user = serializer.validated_data['user'] - - task = self.get_task(task_id, user) - if not task: - return JsonResponse({'error': 'Task not found'}, status=404) - + """ + DELETE http://127.0.0.1:8000/tasks/2/ + """ try: - # Check if task is already completed - if task.is_completed: - return JsonResponse({'error': 'Task already completed'}, status=400) - - # Mark task as completed - task.is_completed = True - task.save() - - # Return updated task - category = task.task_categories.first().category if task.task_categories.first() else None - response_data = { - "id": task.task_id, - "content": task.title, - "is_done": True, - "dpc": { - "deadline": task.deadline.replace(tzinfo=None).isoformat(timespec='seconds') if task.deadline else None, - "priority": task.priority, - "category_id": category.category_id if category else 0, - "category_name": category.name if category else "" - }, - "subtasks": [ - { - "id": s.subtask_id, - "content": s.text, - "is_done": s.is_completed - } for s in task.subtasks.all() - ] - } - return JsonResponse(response_data) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) + token = get_token(request) + return task_response(complete_task(token=token, task_id=task_id), 200) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) def patch(self, request, task_id, *args, **kwargs): - # Get token from request and validate user - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse({'error': 'Invalid token'}, status=401) - user = serializer.validated_data['user'] - - task = self.get_task(task_id, user) - if not task: - return JsonResponse({'error': 'Task not found'}, status=404) - + """ + PATCH http://127.0.0.1:8000/tasks/1/ + { + "content": "Подготовить отчетфыфыфыф", + "dpc": { "deadline": "2025-04-30T18:00:00Z", "priority": 3, "category_id": 5 } + } + """ try: + token = get_token(request) data = json.loads(request.body) - # Update allowed task fields - if 'content' in data: - task.title = data['content'] - if 'dpc' in data: - dpc = data['dpc'] - if 'deadline' in dpc: - task.deadline = parse_datetime(dpc['deadline']) if dpc['deadline'] else None - if 'priority' in dpc: - task.priority = dpc['priority'] - if 'category_id' in dpc: - task.task_categories.all().delete() - if dpc['category_id']: - try: - # Проверяем, что категория принадлежит текущему пользователю - category = Category.objects.get( - category_id=dpc['category_id'], - user=user - ) - TaskCategory.objects.create(task=task, category=category) - except Category.DoesNotExist: - return JsonResponse( - {'error': 'Category not found or access denied'}, - status=400 - ) - task.save() - # Form response with category name from DB - category = task.task_categories.first().category if task.task_categories.first() else None - response_data = { - "id": task.task_id, - "content": task.title, - "is_done": task.is_completed, - "dpc": { - "deadline": task.deadline.replace(tzinfo=None).isoformat(timespec='seconds') if task.deadline else None, - "priority": task.priority, - "category_id": category.category_id if category else 0, - "category_name": category.name if category else "" - }, - "subtasks": [ - { - "id": s.subtask_id, - "content": s.text, - "is_done": s.is_completed - } for s in task.subtasks.all() - ] - } - return JsonResponse(response_data) - except json.JSONDecodeError: - return JsonResponse({'error': 'Invalid JSON'}, status=400) - except Exception as e: + return task_response(update_task(token=token, task_id=task_id, data=data), 200) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except ValidationError as e: return JsonResponse({'error': str(e)}, status=400) - - -# /subtasks - POST -#http://127.0.0.1:8000/subtasks/?task_id=3 -#{ -# "content": "Новая подзадача2", -# "is_done": false -# } - -class SubtaskCreateView(APIView): - def post(self, request, *args, **kwargs): - try: - # Get token from request and validate user - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse({'error': 'Invalid token'}, status=401) - user = serializer.validated_data['user'] - - # Get task_id from request parameters - task_id = request.GET.get('task_id') - if not task_id: - return JsonResponse({'error': 'task_id parameter is required'}, status=400) - - # Check if task exists and belongs to user - try: - task = Task.objects.get(task_id=task_id, user=user) - except Task.DoesNotExist: - return JsonResponse({'error': 'Task not found'}, status=404) - - # Parse request body - data = json.loads(request.body) - content = data.get('content') - is_done = data.get('is_done', False) - - if not content: - return JsonResponse({'error': 'content is required'}, status=400) - - # Create subtask - subtask = Subtask.objects.create( - text=content, - task=task, - is_completed=is_done - ) - - # Prepare response - response_data = { - "id": subtask.subtask_id, - "content": subtask.text, - "is_done": subtask.is_completed - } - - return JsonResponse(response_data, status=201) - - except json.JSONDecodeError: - return JsonResponse({'error': 'Invalid JSON'}, status=400) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - -#PATCH (/subtasks/{subtask_id}) -#http://127.0.0.1:8000/subtasks/4/ -#{ -# "content": "Обновленный текст подзадачи", -# "is_done": true -# } -#DELETE -#http://127.0.0.1:8000/subtasks/4/ -#/subtasks/{subtask_id} -class SubtaskDetailView(APIView): - def get_subtask(self, subtask_id, user): - try: - return Subtask.objects.get(subtask_id=subtask_id, task__user=user) - except Subtask.DoesNotExist: - return None - - def patch(self, request, subtask_id, *args, **kwargs): - try: - # Get token from request and validate user - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse({'error': 'Invalid token'}, status=401) - user = serializer.validated_data['user'] - - # Get subtask - subtask = self.get_subtask(subtask_id, user) - if not subtask: - return JsonResponse({'error': 'Subtask not found'}, status=404) - - # Parse request body - data = json.loads(request.body) - - # Update text if present in request - if 'content' in data: - subtask.text = data['content'] - - # Update status if present in request - if 'is_done' in data: - subtask.is_completed = data['is_done'] - - subtask.save() - - # Prepare response - response_data = { - "id": subtask.subtask_id, - "content": subtask.text, - "is_done": subtask.is_completed - } - - return JsonResponse(response_data) - except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON'}, status=400) except Exception as e: return JsonResponse({'error': str(e)}, status=500) - - def delete(self, request, subtask_id, *args, **kwargs): - try: - # Get token from request and validate user - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse({'error': 'Invalid token'}, status=401) - user = serializer.validated_data['user'] - - # Get and delete subtask - subtask = self.get_subtask(subtask_id, user) - if not subtask: - return JsonResponse({'error': 'Subtask not found'}, status=404) - - subtask.delete() - return HttpResponse(status=204) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - -# GET -# http://127.0.0.1:8000/categories/ -# POST -# { -# "name": "Хехе" -# } -class CategoryListView(APIView): - def get(self, request, *args, **kwargs): - try: - # Аутентификация - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse({'error': 'Invalid token'}, status=401) - user = serializer.validated_data['user'] - - categories = Category.objects.filter(user=user) - - data = [{ - "id": category.category_id, - "name": category.name - } for category in categories] - - return JsonResponse(data, safe=False) - - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - - def post(self, request, *args, **kwargs): - try: - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse({'error': 'Invalid token'}, status=401) - user = serializer.validated_data['user'] - - data = json.loads(request.body) - category_name = data.get('name') - - if not category_name: - return JsonResponse({'error': 'Name is required'}, status=400) - - if len(category_name) > 50: - return JsonResponse({'error': 'Category name too long (max 50 chars)'}, status=400) - - if Category.objects.filter(user=user, name=category_name).exists(): - return JsonResponse({'error': 'Category already exists'}, status=409) - - # Создание категории - category = Category.objects.create( - name=category_name, - user=user - ) - - return JsonResponse({ - "id": category.category_id, - "name": category.name - }, status=201) - - except json.JSONDecodeError: - return JsonResponse({'error': 'Invalid JSON'}, status=400) - except Exception as e: - return JsonResponse({'error': str(e)}, status=500) \ No newline at end of file diff --git a/backend/taskbench/views/user_views.py b/backend/taskbench/views/user_views.py index c6ee5fa1..8351f57b 100644 --- a/backend/taskbench/views/user_views.py +++ b/backend/taskbench/views/user_views.py @@ -1,103 +1,62 @@ import json +import uuid +from datetime import timedelta from django.http import JsonResponse +from django.utils import timezone +from pydantic import ValidationError from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework_simplejwt.tokens import RefreshToken -from ..models.models import User, Subscription - -from ..serializers.user_serializers import UserRegisterSerializer, LoginSerializer, JwtSerializer -from ..services.jwt_service import get_token_from_request -from datetime import datetime, timedelta -import uuid -from django.utils import timezone +from ..models.models import Subscription +from ..serializers.user_serializers import JwtSerializer, user_response +from ..services.user_service import get_token, register_user, login_user, token_refresh, delete_user, change_password, \ + AuthenticationError class RegisterView(APIView): def post(self, request, *args, **kwargs): data = json.loads(request.body) - serializer = UserRegisterSerializer(data=data) - if serializer.is_valid(): - user = serializer.save() - - refresh = RefreshToken.for_user(user) - access_token = str(refresh.access_token) - - return JsonResponse({ - 'user_id': user.user_id, - 'access': access_token, - 'refresh': str(refresh), - }, status=201) - return JsonResponse(serializer.errors, status=400) + try: + return user_response(*register_user(data), status=201) + except Exception as e: + return JsonResponse({'error': str(e)}, status=400) class LoginView(APIView): def post(self, request, *args, **kwargs): data = json.loads(request.body) - serializer = LoginSerializer(data=data) - - if serializer.is_valid(): - user = serializer.validated_data['user'] - refresh = RefreshToken.for_user(user) - access_token = str(refresh.access_token) - refresh_token = str(refresh) - return JsonResponse({ - 'user_id': user.user_id, - 'access': access_token, - 'refresh': refresh_token - }, status=200) + try: + return user_response(*login_user(data), status=200) + except Exception as e: + return JsonResponse({'error': str(e)}, status=400) - return Response(serializer.errors, status=400) class DeleteUserView(APIView): def delete(self, request, *args, **kwargs): - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if serializer.is_valid(): - user = serializer.validated_data['user'] - user.delete() + token = get_token(request) + try: + delete_user(token) return Response(status=204) - return Response(serializer.errors, status=400) - # user = request.user - # user.delete() - # return Response(status=204) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) class ChangePasswordView(APIView): def patch(self, request, *args, **kwargs): - old_password = request.data.get('old_password') - new_password = request.data.get('new_password') - - if not old_password or not new_password: - return JsonResponse( - {"error": "Both old_password and new_password are required"}, - status=400 - ) - - token = get_token_from_request(request) - serializer = JwtSerializer(data=token) - if not serializer.is_valid(): - return JsonResponse( - {"error": "Not logged in"}, - status=401 - ) - user = serializer.validated_data['user'] - - if not user.check_password(old_password): - return JsonResponse( - {"error": "Old password is incorrect"}, - status=400 - ) - - user.set_password(new_password) - user.save() + data = json.loads(request.body) + try: + change_password(token=get_token(request), data=data) + return Response(status=204) + except AuthenticationError as e: + return JsonResponse({'error': str(e)}, status=401) + except ValidationError as e: + return JsonResponse({'error': str(e)}, status=400) - return Response(status=204) class TokenRefreshView(APIView): @@ -106,24 +65,15 @@ def post(self, request, *args, **kwargs): token = {'token': str(data['refresh'])} if data['refresh'] is None: return Response('No token provided.', status=400) - - serializer = JwtSerializer(data=token) - if serializer.is_valid(): - user = serializer.validated_data['user'] - refresh = RefreshToken.for_user(user) - access_token = str(refresh.access_token) - return JsonResponse({ - 'access': access_token, - 'user_id': user.user_id, - 'refresh': str(refresh), - }, status=200) - return Response(serializer.errors, status=400) + try: + return user_response(*token_refresh(token), status=200) + except Exception as e: + return JsonResponse({'error': str(e)}, status=400) class SubscriptionStatusView(APIView): def get(self, request): - # Проверка JWT токена - token = get_token_from_request(request) + token = get_token(request) serializer = JwtSerializer(data=token) if not serializer.is_valid(): return JsonResponse({'error': 'Invalid token'}, status=401) @@ -137,7 +87,6 @@ def get(self, request): end_date__gte=now ).first() - # Формируем ответ if active_subscription: return JsonResponse({ 'has_subscription': True, @@ -154,17 +103,15 @@ def get(self, request): class CreateSubscriptionView(APIView): def post(self, request): # Проверка JWT токена - token = get_token_from_request(request) + token = get_token(request) serializer = JwtSerializer(data=token) if not serializer.is_valid(): return JsonResponse({'error': 'Invalid token'}, status=401) user = serializer.validated_data['user'] - # Создаем тестовую подписку now = timezone.now() - end_date = now + timedelta(days=30) # Подписка на 1 месяц + end_date = now + timedelta(days=30) - # В CreateSubscriptionView перед созданием if user.subscriptions.filter(is_active=True, end_date__gte=now).exists(): return JsonResponse({'error': 'User already has active subscription'}, status=400) @@ -173,7 +120,7 @@ def post(self, request): start_date=now, end_date=end_date, is_active=True, - transaction_id=str(uuid.uuid4()) # Генерируем случайный transaction_id + transaction_id=str(uuid.uuid4()) ) return JsonResponse({ @@ -182,4 +129,4 @@ def post(self, request): 'start_date': subscription.start_date, 'end_date': subscription.end_date, 'transaction_id': subscription.transaction_id - }, status=201) \ No newline at end of file + }, status=201) diff --git a/backend/templates/dashboard/dashboard.html b/backend/templates/dashboard/dashboard.html index 90b14a97..03853d3e 100644 --- a/backend/templates/dashboard/dashboard.html +++ b/backend/templates/dashboard/dashboard.html @@ -56,32 +56,6 @@ width: 500px; } -
{#