a573ed78a4
- 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.
266 lines
9.0 KiB
Python
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
|