Загрузка данных
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)