1
0

vault backup: 2025-12-26 02:09:22
All checks were successful
Deploy Quartz site to GitHub Pages / build (push) Successful in 2m29s

This commit is contained in:
2025-12-26 02:09:22 +01:00
parent 3fddadfe50
commit 50366b9b9c
288 changed files with 58893 additions and 750 deletions

View File

View File

@@ -0,0 +1,184 @@
import pytest
from django.contrib.auth.models import User
from django.urls import reverse
from quiz.models import QuizUser, Question, Option, QuizResult
@pytest.mark.django_db
@pytest.mark.admin
class TestAdminPages:
"""Test that all admin pages render without errors"""
@pytest.fixture
def admin_client(self, client, django_user_model, db):
"""Create authenticated admin client"""
admin_user = django_user_model.objects.create_superuser(
username='testadmin',
email='admin@test.com',
password='admin123'
)
client.force_login(admin_user)
return client
@pytest.fixture
def test_data(self, db):
"""Create test data"""
quiz_user = QuizUser.objects.create(session_key='test_session_123')
question = Question.objects.create(
file_path='test/question1.md',
text='Test question?',
correct_answer='A',
file_mtime=1234567890.0
)
Option.objects.create(question=question, letter='A', text='Correct answer')
Option.objects.create(question=question, letter='B', text='Wrong answer')
quiz_result = QuizResult.objects.create(
user=quiz_user,
question=question,
selected_answer='A',
is_correct=True
)
return {
'quiz_user': quiz_user,
'question': question,
'quiz_result': quiz_result
}
def test_admin_index(self, admin_client):
"""Test admin index page"""
response = admin_client.get(reverse('admin:index'))
assert response.status_code == 200
assert 'Site administration' in response.content.decode()
def test_question_changelist(self, admin_client, test_data):
"""Test Question list page"""
response = admin_client.get(reverse('admin:quiz_question_changelist'))
assert response.status_code == 200
assert 'Test question?' in response.content.decode()
def test_question_add(self, admin_client):
"""Test Question add page"""
response = admin_client.get(reverse('admin:quiz_question_add'))
assert response.status_code == 200
assert 'Add question' in response.content.decode()
def test_question_change(self, admin_client, test_data):
"""Test Question change/edit page"""
response = admin_client.get(
reverse('admin:quiz_question_change', args=[test_data['question'].pk])
)
assert response.status_code == 200
assert 'Test question?' in response.content.decode()
assert 'Correct answer' in response.content.decode()
def test_question_delete(self, admin_client, test_data):
"""Test Question delete page"""
response = admin_client.get(
reverse('admin:quiz_question_delete', args=[test_data['question'].pk])
)
assert response.status_code == 200
assert 'Are you sure' in response.content.decode()
def test_option_add(self, admin_client):
"""Test Option add page"""
response = admin_client.get(reverse('admin:quiz_option_add'))
assert response.status_code == 200
assert 'Add option' in response.content.decode()
def test_option_change(self, admin_client, test_data):
"""Test Option change/edit page"""
option = test_data['question'].options.first()
response = admin_client.get(
reverse('admin:quiz_option_change', args=[option.pk])
)
assert response.status_code == 200
assert 'Correct answer' in response.content.decode()
def test_quizuser_changelist(self, admin_client, test_data):
"""Test QuizUser list page"""
response = admin_client.get(reverse('admin:quiz_quizuser_changelist'))
assert response.status_code == 200
assert 'test_session' in response.content.decode()
def test_quizuser_add(self, admin_client):
"""Test QuizUser add page"""
response = admin_client.get(reverse('admin:quiz_quizuser_add'))
assert response.status_code == 200
assert 'Add Quiz User' in response.content.decode()
def test_quizuser_change(self, admin_client, test_data):
"""Test QuizUser change/edit page"""
response = admin_client.get(
reverse('admin:quiz_quizuser_change', args=[test_data['quiz_user'].pk])
)
assert response.status_code == 200
assert 'test_session' in response.content.decode()
def test_quizresult_changelist(self, admin_client, test_data):
"""Test QuizResult list page"""
response = admin_client.get(reverse('admin:quiz_quizresult_changelist'))
assert response.status_code == 200
assert 'Test question' in response.content.decode()
def test_quizresult_add(self, admin_client):
"""Test QuizResult add page"""
response = admin_client.get(reverse('admin:quiz_quizresult_add'))
assert response.status_code == 200
assert 'Add quiz result' in response.content.decode()
def test_quizresult_change(self, admin_client, test_data):
"""Test QuizResult change/edit page"""
response = admin_client.get(
reverse('admin:quiz_quizresult_change', args=[test_data['quiz_result'].pk])
)
assert response.status_code == 200
assert 'Test question' in response.content.decode()
def test_admin_custom_displays(self, admin_client, test_data):
"""Test custom admin display methods render correctly"""
# Question admin with custom fields
response = admin_client.get(reverse('admin:quiz_question_changelist'))
assert 'question1.md' in response.content.decode()
# QuizUser admin with score percentage
response = admin_client.get(reverse('admin:quiz_quizuser_changelist'))
assert '100.0%' in response.content.decode()
# QuizResult admin with result status
response = admin_client.get(reverse('admin:quiz_quizresult_changelist'))
assert 'Correct' in response.content.decode()
def test_admin_search(self, admin_client, test_data):
"""Test admin search functionality"""
response = admin_client.get(
reverse('admin:quiz_question_changelist') + '?q=Test'
)
assert response.status_code == 200
assert 'Test question?' in response.content.decode()
def test_admin_filters(self, admin_client, test_data):
"""Test admin filter functionality"""
response = admin_client.get(
reverse('admin:quiz_quizresult_changelist') + '?is_correct__exact=1'
)
assert response.status_code == 200
@pytest.mark.parametrize('url_name', [
'admin:index',
'admin:quiz_question_changelist',
'admin:quiz_question_add',
'admin:quiz_quizuser_changelist',
'admin:quiz_quizuser_add',
'admin:quiz_quizresult_changelist',
'admin:quiz_quizresult_add',
])
def test_all_admin_pages_no_errors(self, admin_client, test_data, url_name):
"""Integration test: verify no admin pages return errors"""
url = reverse(url_name)
response = admin_client.get(url)
assert response.status_code == 200, f"Failed to load {url}"

View File

@@ -0,0 +1,85 @@
import pytest
from django.urls import reverse
from quiz.models import Course, Tag, Question, Exam, QuizUser, QuizSession
from quiz.forms import CreateQuizForm
@pytest.mark.django_db
class TestQuizCreation:
@pytest.fixture(autouse=True)
def setup_data(self, client: pytest.FixtureRequest) -> None:
# Clear database to ensure fresh state
Question.objects.all().delete()
Tag.objects.all().delete()
QuizSession.objects.all().delete()
Course.objects.all().delete()
self.client = client
s = self.client.session
s.save()
self.user = QuizUser.objects.create(session_key=s.session_key)
self.course1 = Course.objects.create(name="Course 1", code="C1")
self.course2 = Course.objects.create(name="Course 2", code="C2")
self.tag1 = Tag.objects.create(name="Tag1", slug="tag-1")
self.exam1 = Exam.objects.create(course=self.course1, date="2023-01-01")
self.exam2 = Exam.objects.create(course=self.course2, date="2023-01-02")
self.q1 = Question.objects.create(
exam=self.exam1, text="UNIQUE_Q1_TEXT", correct_answer="A", file_path="path1"
)
self.q1.tags.add(self.tag1)
self.q2 = Question.objects.create(
exam=self.exam2, text="UNIQUE_Q2_TEXT", correct_answer="A,B", file_path="path2"
)
def test_create_quiz_form_valid(self) -> None:
form_data = {
'course': self.course1.id,
'tags': [self.tag1.id],
'question_type': ['single']
}
form = CreateQuizForm(data=form_data)
assert form.is_valid()
def test_create_quiz_view_post(self) -> None:
response = self.client.post(reverse('quiz:create_quiz'), {
'course': self.course1.id,
'tags': [self.tag1.id],
'question_type': ['single']
})
session = QuizSession.objects.get(user=self.user)
assert response.status_code == 302
assert response.url == reverse('quiz:quiz_mode', args=[session.id])
assert session.course.id == self.course1.id
assert list(session.tags.values_list('id', flat=True)) == [self.tag1.id]
assert session.question_types == ['single']
def test_get_next_question_filters(self) -> None:
session = QuizSession.objects.create(user=self.user, course=self.course1)
response = self.client.get(reverse('quiz:next_question', args=[session.id]))
assert response.status_code == 200
assert "UNIQUE_Q1_TEXT" in response.content.decode()
# Now change filter to Course 2
session.course = self.course2
session.save()
response = self.client.get(reverse('quiz:next_question', args=[session.id]))
assert "UNIQUE_Q2_TEXT" in response.content.decode()
def test_filter_by_type(self) -> None:
session = QuizSession.objects.create(user=self.user, question_types=['multi'])
response = self.client.get(reverse('quiz:next_question', args=[session.id]))
assert "UNIQUE_Q2_TEXT" in response.content.decode()
session.question_types = ['single']
session.save()
response = self.client.get(reverse('quiz:next_question', args=[session.id]))
assert "UNIQUE_Q1_TEXT" in response.content.decode()

View File

@@ -0,0 +1,307 @@
from django.test import TestCase, Client
from django.urls import reverse
from quiz.models import Question, QuizUser, QuizSession, QuizResult, Course, Exam, Tag, Option
class QuizViewsTestCase(TestCase):
"""Comprehensive tests for all quiz endpoints"""
def setUp(self):
"""Set up test data"""
self.client = Client()
# Ensure session exists and get its key
s = self.client.session
s.save()
session_key = s.session_key
# Create test user with the same session key
self.user = QuizUser.objects.create(session_key=session_key)
# Create test course
self.course = Course.objects.create(name="Test Course")
# Create test exam
self.exam = Exam.objects.create(
name="Test Exam",
course=self.course,
date="2025-01-01" # Required field
)
# Create test tags
self.tag1 = Tag.objects.create(name="Tag 1", slug="tag-1")
self.tag2 = Tag.objects.create(name="Tag 2", slug="tag-2")
# Create test questions
self.question1 = Question.objects.create(
text="Test question 1?",
correct_answer="A",
exam=self.exam,
file_path="test1.md"
)
self.question1.tags.add(self.tag1)
Option.objects.create(question=self.question1, letter="A", text="Answer A")
Option.objects.create(question=self.question1, letter="B", text="Answer B")
self.question2 = Question.objects.create(
text="Test question 2 (multi)?",
correct_answer="A,B",
exam=self.exam,
file_path="test2.md"
)
self.question2.tags.add(self.tag1, self.tag2)
Option.objects.create(question=self.question2, letter="A", text="Answer A")
Option.objects.create(question=self.question2, letter="B", text="Answer B")
Option.objects.create(question=self.question2, letter="C", text="Answer C")
# Set user in session
session = self.client.session
session['quiz_user_id'] = self.user.id
session.save()
def test_index_view(self):
"""Test dashboard index view"""
response = self.client.get(reverse('quiz:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Snabbstart')
self.assertIn('active_sessions', response.context)
self.assertIn('form', response.context)
def test_create_quiz(self):
"""Test quiz creation"""
response = self.client.post(reverse('quiz:create_quiz'), {
'course': self.course.id,
'tags': [self.tag1.id],
})
self.assertEqual(response.status_code, 302) # Redirect
# Verify session was created
session = QuizSession.objects.filter(user=self.user).first()
self.assertIsNotNone(session)
self.assertEqual(session.course, self.course)
self.assertTrue(session.is_active)
def test_quiz_mode_view(self):
"""Test dedicated quiz mode view"""
session = QuizSession.objects.create(
user=self.user,
course=self.course
)
response = self.client.get(reverse('quiz:quiz_mode', args=[session.id]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Course')
self.assertIn('session', response.context)
def test_quiz_question_view(self):
"""Test quiz question endpoint"""
session = QuizSession.objects.create(
user=self.user,
course=self.course
)
response = self.client.get(reverse('quiz:quiz_question', args=[session.id]))
self.assertEqual(response.status_code, 200)
self.assertIn('question', response.context)
self.assertIn('session', response.context)
def test_navigate_question(self):
"""Test question navigation"""
session = QuizSession.objects.create(
user=self.user,
course=self.course
)
# Test next navigation
response = self.client.get(
reverse('quiz:navigate_question', args=[session.id, 'next']),
{'q': self.question1.id}
)
self.assertEqual(response.status_code, 200)
# Test previous navigation
response = self.client.get(
reverse('quiz:navigate_question', args=[session.id, 'previous']),
{'q': self.question2.id}
)
self.assertEqual(response.status_code, 200)
def test_submit_single_answer(self):
"""Test submitting a single-choice answer"""
session = QuizSession.objects.create(
user=self.user,
course=self.course
)
response = self.client.post(
reverse('quiz:submit_answer', args=[session.id]),
{
'question_id': self.question1.id,
'answer': 'A'
}
)
self.assertEqual(response.status_code, 200)
# Verify result was created
result = QuizResult.objects.get(
user=self.user,
question=self.question1,
quiz_session=session
)
self.assertEqual(result.selected_answer, 'A')
self.assertTrue(result.is_correct)
def test_submit_multi_answer(self):
"""Test submitting a multi-choice answer"""
session = QuizSession.objects.create(
user=self.user,
course=self.course
)
# Test correct multi-answer (order shouldn't matter)
response = self.client.post(
reverse('quiz:submit_answer', args=[session.id]),
{
'question_id': self.question2.id,
'answer': 'B,A' # Reversed order
}
)
self.assertEqual(response.status_code, 200)
result = QuizResult.objects.get(
user=self.user,
question=self.question2,
quiz_session=session
)
self.assertTrue(result.is_correct) # Should be correct despite order
def test_submit_difficulty(self):
"""Test submitting FSRS difficulty rating"""
session = QuizSession.objects.create(
user=self.user,
course=self.course
)
# First submit an answer
QuizResult.objects.create(
user=self.user,
question=self.question1,
quiz_session=session,
selected_answer='A',
is_correct=True
)
# Then submit difficulty
response = self.client.post(
reverse('quiz:submit_difficulty', args=[session.id]),
{
'question_id': self.question1.id,
'difficulty': 'easy'
}
)
self.assertEqual(response.status_code, 200)
# Verify difficulty was saved
result = QuizResult.objects.get(
user=self.user,
question=self.question1,
quiz_session=session
)
self.assertEqual(result.difficulty, 'easy')
def test_close_quiz(self):
"""Test closing a quiz session"""
session = QuizSession.objects.create(
user=self.user,
course=self.course,
is_active=True
)
response = self.client.post(reverse('quiz:close_quiz', args=[session.id]))
self.assertEqual(response.status_code, 302) # Redirect to index
# Verify session was deactivated
session.refresh_from_db()
self.assertFalse(session.is_active)
def test_invalid_session_access(self):
"""Test accessing another user's session"""
other_user = QuizUser.objects.create(session_key="other_session_key")
session = QuizSession.objects.create(
user=other_user,
course=self.course
)
# Try to access it
response = self.client.get(reverse('quiz:quiz_mode', args=[session.id]))
self.assertEqual(response.status_code, 404)
def test_answer_without_question_id(self):
"""Test error handling for missing question_id"""
session = QuizSession.objects.create(
user=self.user,
course=self.course
)
response = self.client.post(
reverse('quiz:submit_answer', args=[session.id]),
{'answer': 'A'} # Missing question_id
)
self.assertEqual(response.status_code, 400)
def test_difficulty_without_result(self):
"""Test error handling for difficulty without existing result"""
session = QuizSession.objects.create(
user=self.user,
course=self.course
)
response = self.client.post(
reverse('quiz:submit_difficulty', args=[session.id]),
{
'question_id': self.question1.id,
'difficulty': 'easy'
}
)
self.assertEqual(response.status_code, 404)
def test_answer_normalization(self):
"""Test that multi-choice answers are normalized correctly"""
session = QuizSession.objects.create(
user=self.user,
course=self.course
)
# Submit answers in different orders
test_cases = [
('A,B', True), # Correct order
('B,A', True), # Reversed order
('A,C', False), # Wrong answer
('A', False), # Incomplete answer
]
for answer, expected_correct in test_cases:
result = QuizResult.objects.filter(
user=self.user,
question=self.question2,
quiz_session=session
).delete() # Clean up
response = self.client.post(
reverse('quiz:submit_answer', args=[session.id]),
{
'question_id': self.question2.id,
'answer': answer
}
)
self.assertEqual(response.status_code, 200)
result = QuizResult.objects.get(
user=self.user,
question=self.question2,
quiz_session=session
)
self.assertEqual(result.is_correct, expected_correct,
f"Answer '{answer}' should be {'correct' if expected_correct else 'incorrect'}")