Files
penracourses/generic/tests/test_supervisor_dashboard.py
Ross a573ed78a4 Enhance Supervisor Dashboard and Trainee Feedback Features
- Redesigned the supervisor overview page to include a header card with supervisor details and trainee statistics.
- Implemented a tabbed interface for viewing trainees and their recent attempts.
- Improved the trainee detail page with a profile card and a table for displaying attempt history.
- Added a new view for supervisors to provide feedback on case collections, including outstanding feedback items and previous conversations.
- Updated URL routing to support new feedback functionality.
- Enhanced backend logic to ensure proper sharing permissions for exam attempts and case collections.
- Added comprehensive tests for supervisor access to trainee data and feedback functionalities.
2026-06-15 12:28:00 +01:00

266 lines
9.0 KiB
Python

import pytest
from django.urls import reverse
from django.contrib.auth.models import User
from django.test import Client
from django.contrib.contenttypes.models import ContentType
from generic.models import Supervisor, UserProfile, CidUserExam
from physics.models import Exam as PhysicsExam
from atlas.models import CaseCollection, Case, CaseDetail
@pytest.fixture
def supervisor_user(db):
return User.objects.create_user(username="supervisor_user", password="password", email="supervisor@test.com")
@pytest.fixture
def supervisor_model(db, supervisor_user):
return Supervisor.objects.create(name="Test Supervisor", email="supervisor@test.com", user=supervisor_user)
@pytest.fixture
def other_supervisor_user(db):
return User.objects.create_user(username="other_supervisor", password="password", email="other@test.com")
@pytest.fixture
def other_supervisor(db, other_supervisor_user):
return Supervisor.objects.create(name="Other Supervisor", email="other@test.com", user=other_supervisor_user)
@pytest.fixture
def trainee_user(db, supervisor_model):
user = User.objects.create_user(username="trainee", password="password", email="trainee@test.com")
profile = user.userprofile
profile.supervisor = supervisor_model
profile.save()
return user
@pytest.fixture
def physics_exam(db):
return PhysicsExam.objects.create(name="Test Physics Exam", exam_mode=True)
@pytest.fixture
def case_collection(db):
collection = CaseCollection.objects.create(name="Test Case Collection")
# Enable messaging
collection.feedback_once_collection_complete = True
collection.save()
return collection
@pytest.fixture
def case_model(db):
return Case.objects.create(title="Test Case")
@pytest.fixture
def case_detail(db, case_collection, case_model):
return CaseDetail.objects.create(collection=case_collection, case=case_model, sort_order=0)
@pytest.fixture
def exam_attempt(db, trainee_user, physics_exam):
ct = ContentType.objects.get_for_model(PhysicsExam)
return CidUserExam.objects.create(
content_type=ct,
object_id=physics_exam.pk,
user_user=trainee_user,
share_with_supervisor=False
)
@pytest.fixture
def collection_attempt(db, trainee_user, case_collection):
ct = ContentType.objects.get_for_model(CaseCollection)
return CidUserExam.objects.create(
content_type=ct,
object_id=case_collection.pk,
user_user=trainee_user,
share_with_supervisor=False
)
@pytest.mark.django_db
def test_supervisor_overview_access(
supervisor_user,
other_supervisor_user,
supervisor_model,
trainee_user,
physics_exam,
exam_attempt,
case_collection,
collection_attempt,
):
client = Client()
url = reverse("generic:supervisor_overview", kwargs={"pk": supervisor_model.pk})
# Anonymous blocked
response = client.get(url)
assert response.status_code == 302
# Other supervisor blocked
client.force_login(other_supervisor_user)
response = client.get(url)
assert response.status_code == 403
# Authorized supervisor allowed
client.force_login(supervisor_user)
# First test with private attempts
response = client.get(url)
assert response.status_code == 200
assert b"Test Physics Exam" in response.content
assert b"Test Case Collection" in response.content
assert b"Locked" in response.content
# Now test with shared attempts
exam_attempt.share_with_supervisor = True
exam_attempt.save()
collection_attempt.share_with_supervisor = True
collection_attempt.save()
response = client.get(url)
assert response.status_code == 200
assert b"Test Physics Exam" in response.content
assert b"Test Case Collection" in response.content
assert b"0 / 0 cases" in response.content # collection progress check
@pytest.mark.django_db
def test_supervisor_trainee_access(supervisor_user, other_supervisor_user, supervisor_model, trainee_user):
client = Client()
url = reverse("generic:supervisor_trainee", kwargs={"pk": supervisor_model.pk, "trainee_id": trainee_user.pk})
# Other supervisor blocked
client.force_login(other_supervisor_user)
response = client.get(url)
assert response.status_code == 403
# Authorized supervisor allowed
client.force_login(supervisor_user)
response = client.get(url)
assert response.status_code == 200
@pytest.mark.django_db
def test_exam_scores_user_supervisor_sharing(supervisor_user, other_supervisor_user, supervisor_model, trainee_user, physics_exam, exam_attempt):
client = Client()
url = reverse("physics:exam_scores_user_supervisor", kwargs={"pk": physics_exam.pk, "user_id": trainee_user.pk})
# Other supervisor blocked regardless
client.force_login(other_supervisor_user)
response = client.get(url)
assert response.status_code == 403
# Authorized supervisor blocked when not shared
client.force_login(supervisor_user)
response = client.get(url)
assert response.status_code == 403
# Share and check allowed
exam_attempt.share_with_supervisor = True
exam_attempt.save()
response = client.get(url)
assert response.status_code == 200
# Unshare, but set results_supervisor_visible = True
exam_attempt.share_with_supervisor = False
exam_attempt.save()
physics_exam.results_supervisor_visible = True
physics_exam.save()
response = client.get(url)
assert response.status_code == 200
@pytest.mark.django_db
def test_supervisor_collection_feedback_sharing(supervisor_user, supervisor_model, trainee_user, case_collection, collection_attempt):
client = Client()
url = reverse("atlas:collection_history_user", kwargs={
"exam_id": case_collection.pk,
"user_pk": trainee_user.pk
})
# Supervisor blocked when not shared
client.force_login(supervisor_user)
response = client.get(url)
assert response.status_code == 403
# Supervisor allowed when shared
collection_attempt.share_with_supervisor = True
collection_attempt.save()
response = client.get(url)
assert response.status_code == 200
@pytest.mark.django_db
def test_collection_case_review_thread_supervisor_access(supervisor_user, supervisor_model, trainee_user, case_collection, collection_attempt, case_model, case_detail):
client = Client()
url = reverse("atlas:collection_case_review_thread", kwargs={
"exam_id": case_collection.pk,
"cid_user_exam_id": collection_attempt.pk,
"case_id": case_model.pk
})
# Blocked when attempt is not shared
client.force_login(supervisor_user)
response = client.get(url)
assert response.status_code == 403
# Allowed when shared
collection_attempt.share_with_supervisor = True
collection_attempt.save()
response = client.get(url)
assert response.status_code == 200
# Post a feedback reply as supervisor
response = client.post(url, {
"action": "add_message",
"message_type": "FB",
"message": "Nice job trainee!"
}, HTTP_X_REQUESTED_WITH="XMLHttpRequest")
assert response.status_code == 200
assert b"Nice job trainee!" in response.content
@pytest.mark.django_db
def test_collection_case_view_take_review_user(
supervisor_user,
other_supervisor_user,
supervisor_model,
trainee_user,
case_collection,
case_model,
case_detail,
collection_attempt,
):
"""Supervisors can view a trainee's case answers in read-only mode.
- Unshared attempts are blocked (403).
- Shared attempts return 200 with the reviewer banner present.
- Other supervisors with no relationship to the trainee are blocked.
- A collection author can always view the answers.
"""
client = Client()
url = reverse(
"atlas:collection_case_view_take_review_user",
kwargs={"pk": case_collection.pk, "case_number": 0, "user_pk": trainee_user.pk},
)
# Anonymous user is redirected to login
response = client.get(url)
assert response.status_code == 302
# Authorized supervisor blocked when attempt not shared
client.force_login(supervisor_user)
response = client.get(url)
assert response.status_code == 403
# Authorized supervisor allowed when attempt is shared
collection_attempt.share_with_supervisor = True
collection_attempt.save()
response = client.get(url)
assert response.status_code == 200
# Reviewer banner should be present
assert b"Reviewer mode" in response.content
assert b"read-only" in response.content
# Other supervisor (not linked to trainee) is still blocked even when shared
client.force_login(other_supervisor_user)
response = client.get(url)
assert response.status_code == 403
# Collection author can always view the answers
author_user = User.objects.create_user(username="author_user", password="password")
case_collection.author.add(author_user)
client.force_login(author_user)
response = client.get(url)
assert response.status_code == 200
assert b"Reviewer mode" in response.content