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