Загрузка данных


import uuid
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase
from .models import CustomUser, Course, Enrollment, Assignment, Submission

class LMSFullCrudTests(APITestCase):

    def setUp(self):
        # Создание ролей
        self.admin = CustomUser.objects.create_user(
            username='admin', email='admin@test.com', password='Password123', role='admin'
        )
        self.teacher_owner = CustomUser.objects.create_user(
            username='teacher_owner', email='teacher1@test.com', password='Password123', role='teacher'
        )
        self.teacher_alien = CustomUser.objects.create_user(
            username='teacher_alien', email='teacher2@test.com', password='Password123', role='teacher'
        )
        self.student = CustomUser.objects.create_user(
            username='student', email='student@test.com', password='Password123', role='student'
        )
        self.other_student = CustomUser.objects.create_user(
            username='other_student', email='other_s@test.com', password='Password123', role='student'
        )

        # Создание базовых объектов для CRUD-тестов
        self.course = Course.objects.create(
            title='Base Course', description='Description long enough for validators', teacher=self.teacher_owner
        )
        self.enrollment = Enrollment.objects.create(
            student=self.student, course=self.course, status='active'
        )
        self.assignment = Assignment.objects.create(
            course=self.course, title='Base Assignment', description='Do the work', max_score=100,
            due_date=timezone.now() + timezone.timedelta(days=2)
        )
        self.submission = Submission.objects.create(
            assignment=self.assignment, student=self.student, content='Valid length student solution text'
        )

        # Константы путей
        self.register_url = reverse('register')
        self.login_url = reverse('login')
        self.profile_url = reverse('user-profile')
        self.logout_url = reverse('logout')

    def auth(self, user):
        """Хелпер авторизации"""
        response = self.client.post(self.login_url, {'username': user.username, 'password': 'Password123'})
        token = response.data['data']['token']
        self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')

    # ======================================================================
    # 1. АУТЕНТИФИКАЦИЯ & ПРОФИЛЬ (POST, GET)
    # ======================================================================

    def test_auth_flow(self):
        # POST: Регистрация (Пароль изменен на сложный, чтобы пройти validate_password)
        reg_data = {"username": "u", "email": "e@t.com", "password": "Pr0fessor_St3p@n_2026!", "role": "student"}
        res = self.client.post(self.register_url, reg_data)
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)

        # POST: Логин
        res = self.client.post(self.login_url, {"username": "u", "password": "Pr0fessor_St3p@n_2026!"})
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        token = res.data['data']['token']

        # GET: Профиль
        self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}')
        res = self.client.get(self.profile_url)
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data['data']['username'], "u")

        # POST: Выход
        res = self.client.post(self.logout_url)
        self.assertEqual(res.status_code, status.HTTP_200_OK)

    # ======================================================================
    # 2. КУРСЫ (GET, POST, PUT, PATCH, DELETE)
    # ======================================================================

    def test_course_crud(self):
        # GET: Список
        res = self.client.get(reverse('courses-list'))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data['message'], "Course list")

        # POST: Создание (Запрещено студенту)
        self.auth(self.student)
        res = self.client.post(reverse('courses-list'), {"title": "X", "description": "Y"})
        self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)

        # POST: Создание (Разрешено преподавателю)
        self.auth(self.teacher_owner)
        res = self.client.post(reverse('courses-list'), {"title": "New Course", "description": "Valid course description"})
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        new_course_id = res.data['data']['id']

        # GET: Детализация
        res = self.client.get(f"/api/courses/{new_course_id}/")
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data['message'], "Course detail")

        # PUT: Полное обновление (Запрещено чужому учителю)
        self.auth(self.teacher_alien)
        res = self.client.put(f"/api/courses/{new_course_id}/", {"title": "Edited", "description": "Completely new description"})
        self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)

        # PATCH: Частичное обновление (Разрешено владельцу)
        self.auth(self.teacher_owner)
        res = self.client.patch(f"/api/courses/{new_course_id}/", {"title": "Patched Title"})
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data['data']['title'], "Patched Title")

        # DELETE: Удаление (Разрешено админу / владельцу)
        self.auth(self.admin)
        res = self.client.delete(f"/api/courses/{new_course_id}/")
        self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)

    # ======================================================================
    # 3. ЗАПИСИ НА КУРС (POST, GET, DELETE)
    # ======================================================================

    def test_enrollment_crud(self):
        # POST: Запись на курс (Вложенный экшен)
        self.auth(self.other_student)
        res = self.client.post(f"/api/courses/{self.course.id}/enroll/")
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        self.assertEqual(res.data['message'], "Enrollment created")
        temp_enroll_id = res.data['data']['id']

        # GET: Просмотр списка записей
        self.auth(self.student)
        res = self.client.get(reverse('enrollments-list'))
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        
        # GET: Детализация записи
        res = self.client.get(f"/api/enrollments/{self.enrollment.id}/")
        self.assertEqual(res.status_code, status.HTTP_200_OK)

        # DELETE: Отмена записи студентом (Логическое удаление -> status: cancelled)
        res = self.client.delete(f"/api/enrollments/{self.enrollment.id}/")
        self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
        self.enrollment.refresh_from_db()
        self.assertEqual(self.enrollment.status, 'cancelled')

        # DELETE: Полное физическое удаление записи админом
        self.auth(self.admin)
        res = self.client.delete(f"/api/enrollments/{temp_enroll_id}/")
        self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)
        self.assertFalse(Enrollment.objects.filter(id=temp_enroll_id).exists())

    # ======================================================================
    # 4. ДОМАШНИЕ ЗАДАНИЯ (GET, POST, PUT, PATCH, DELETE)
    # ======================================================================

    def test_assignment_crud(self):
        # GET: Просмотр списка заданий конкретного курса
        res = self.client.get(f"/api/courses/{self.course.id}/assignments/")
        self.assertEqual(res.status_code, status.HTTP_200_OK)

        # POST: Создание задания внутри курса
        self.auth(self.teacher_owner)
        res = self.client.post(f"/api/courses/{self.course.id}/assignments/", {
            "title": "New Task", "description": "Task description details", 
            "max_score": 50, "due_date": "2026-07-01T00:00:00Z"
        })
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        task_id = res.data['data']['id']

        # GET: Получение деталей задания по общему эндпоинту
        res = self.client.get(f"/api/assignments/{task_id}/")
        self.assertEqual(res.status_code, status.HTTP_200_OK)

        # PUT: Полное изменение параметров задания
        res = self.client.put(f"/api/assignments/{task_id}/", {
            "title": "Updated Entirely", "description": "Completely updated task body details",
            "max_score": 100, "due_date": "2026-08-01T00:00:00Z"
        })
        self.assertEqual(res.status_code, status.HTTP_200_OK)

        # PATCH: Частичное изменение
        res = self.client.patch(f"/api/assignments/{task_id}/", {"max_score": 80})
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data['data']['max_score'], 80)

        # DELETE: Удаление задания
        res = self.client.delete(f"/api/assignments/{task_id}/")
        self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)

    # ======================================================================
    # 5. ОТПРАВКА И ОЦЕНКА РЕШЕНИЙ (POST, GET, PATCH, DELETE)
    # ======================================================================

    def test_submission_and_grading_crud(self):
        # Подготовка: Создаем новое задание
        new_assignment = Assignment.objects.create(
            course=self.course, title='Unique Sub Task', description='Desc', max_score=100,
            due_date=timezone.now() + timezone.timedelta(days=1)
        )

        # POST: Сдача решения студентом
        self.auth(self.student)
        self.enrollment.status = 'active'
        self.enrollment.save()

        # Теперь роутер корректно обработает множественное число /api/assignments/...
        res = self.client.post(f"/api/assignments/{new_assignment.id}/submissions/", {"content": "Student submission code link"})
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        sub_id = res.data['data']['id']

        # GET: Просмотр списка решений преподавателем
        self.auth(self.teacher_owner)
        res = self.client.get(reverse('submissions-list'))
        self.assertEqual(res.status_code, status.HTTP_200_OK)

        # GET: Детализация сдачи решения по ID
        res = self.client.get(f"/api/submissions/{sub_id}/")
        self.assertEqual(res.status_code, status.HTTP_200_OK)

        # PATCH: Выставление оценки через выделенный эндпоинт /grade/
        res = self.client.patch(f"/api/submissions/{sub_id}/grade/", {"score": 90, "teacher_comment": "Good job"})
        self.assertEqual(res.status_code, status.HTTP_200_OK)
        self.assertEqual(res.data['data']['score'], 90)

        # DELETE: Удаление сдачи
        self.auth(self.admin)
        res = self.client.delete(f"/api/submissions/{sub_id}/")
        self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT)