+
+
+
+ Dashboard
+ {{ trainee.first_name }} {{ trainee.last_name }}
+
+
-
Trainee: {{trainee.first_name}} {{trainee.last_name}}
-email: {{trainee.email}}
+
+
+
+
+
+
+
+
+
{{ trainee.first_name }} {{ trainee.last_name }}
+
{{ trainee.email }}
+
+
+
+
+
+
Grade
+
{{ trainee.userprofile.grade|default:"N/A" }}
+
+
+
Primary Site
+
{{ trainee.userprofile.site|default:"N/A" }}
+
+
+
Attempts
+
{{ attempts|length }}
+
+
+
+
-
-
Results
-
- {% if all_exams %}
- The following exam results for the trainee are available.
- {% for exam_type, exams in all_exams %}
- {% if exams %}
-
{{exam_type|title}}
-
- {% for exam in exams %}
-
-
-
-
- {{exam}} {% if exam.active %}[Active]{% endif %} {% if exam.publish_results %}[Results Published]{% endif %}
- {% endfor %}
-
- {% endif %}
- {% endfor %}
- {% else %}
- No results are available for this trainee.
-{% endif %}
+
+
+
+
+
+
+
+
+ Attempt Item
+ Type
+ Date Attempted
+ Sharing Status
+ Score / Progress
+ Action
+
+
+
+ {% for item in attempts %}
+
+ {{ item.exam }}
+
+
+ {{ item.type_display }}
+
+
+ {{ item.date|date:"d M Y H:i" }}
+
+ {% if item.is_shared %}
+ Shared
+ {% else %}
+ Private
+ {% endif %}
+
+
+ {% if item.is_shared %}
+ {{ item.score_str }}
+ {% else %}
+ Locked (Private)
+ {% endif %}
+
+
+ {% if item.is_shared and item.detail_url %}
+
+ View Details
+
+ {% else %}
+
+ Locked
+
+ {% endif %}
+
+
+ {% empty %}
+
+
+
+ This trainee has not started any attempts yet.
+
+
+ {% endfor %}
+
+
+
+
+
-
-
{% endblock %}
-
diff --git a/generic/tests/test_exam_collection.py b/generic/tests/test_exam_collection.py
index 5b924ad4..7851ea27 100644
--- a/generic/tests/test_exam_collection.py
+++ b/generic/tests/test_exam_collection.py
@@ -11,6 +11,9 @@ import datetime
class TestExamCollections:
def test_list_url(self, db, client):
+ from django.contrib.auth.models import User
+ user = User.objects.create_user(username="test_list_user", password="password")
+ client.force_login(user)
# Test with no content
response = client.get(reverse("generic:examcollection_list"))
assert response.status_code == 200
@@ -22,11 +25,12 @@ class TestExamCollections:
assert response.status_code == 404
collection = ExamCollection.objects.create(name="test collection", date=datetime.date.today())
+ collection.author.add(user)
response = client.get(reverse("generic:examcollection_list"))
assert response.status_code == 200
soup = BeautifulSoup(response.content, "html.parser")
- assert collection.name in soup.find("li", class_="collection").text
+ assert collection.name in soup.find("a", class_="stretched-link").text
response = client.get(reverse("generic:examcollection_detail", args=( collection.pk, )))
diff --git a/generic/tests/test_supervisor_dashboard.py b/generic/tests/test_supervisor_dashboard.py
new file mode 100644
index 00000000..975645bb
--- /dev/null
+++ b/generic/tests/test_supervisor_dashboard.py
@@ -0,0 +1,265 @@
+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
diff --git a/generic/urls.py b/generic/urls.py
index ca1e8b36..088d36e0 100755
--- a/generic/urls.py
+++ b/generic/urls.py
@@ -239,6 +239,11 @@ urlpatterns = [
views.supervisor_trainee,
name="supervisor_trainee",
),
+ path(
+ "supervisor/
/trainee//collection//",
+ views.supervisor_collection_feedback,
+ name="supervisor_collection_feedback",
+ ),
path(
"supervisor/",
views.SupervisorDetail.as_view(),
diff --git a/generic/views.py b/generic/views.py
index 06194589..ab0ea1e8 100644
--- a/generic/views.py
+++ b/generic/views.py
@@ -380,6 +380,126 @@ class CidManagerRequiredMixin(UserPassesTestMixin):
# raise PermissionDenied() # or Http404
+def get_exam_attempt_score(exam_type, exam, user):
+ from django.contrib.contenttypes.models import ContentType
+ from physics.models import UserAnswer as PhysicsUserAnswer
+ from anatomy.models import UserAnswer as AnatomyUserAnswer
+ from rapids.models import UserAnswer as RapidsUserAnswer
+ from shorts.models import UserAnswer as ShortsUserAnswer
+ from longs.models import UserAnswer as LongsUserAnswer
+ from sbas.models import UserAnswer as SbasUserAnswer
+
+ MAP = {
+ "physics": PhysicsUserAnswer,
+ "anatomy": AnatomyUserAnswer,
+ "rapids": RapidsUserAnswer,
+ "shorts": ShortsUserAnswer,
+ "longs": LongsUserAnswer,
+ "sbas": SbasUserAnswer,
+ }
+ UserAnswer = MAP.get(exam_type)
+ if not UserAnswer:
+ return None, None
+
+ questions = list(exam.get_questions())
+ if not questions:
+ return 0, 0
+ user_answers = UserAnswer.objects.filter(user=user, exam=exam).select_related("question")
+ user_answers_map = {ua.question_id: ua for ua in user_answers}
+
+ total_score = 0
+ for q in questions:
+ ua = user_answers_map.get(q.pk)
+ if ua:
+ total_score += ua.get_answer_score()
+ else:
+ score, _ = q.get_unanswered_mark_and_text()
+ total_score += score
+
+ # Max score
+ match exam_type:
+ case "rapids" | "anatomy":
+ max_score = len(questions) * 2
+ case "longs":
+ max_score = len(questions) * 8
+ case "physics" | "shorts":
+ max_score = len(questions) * 5
+ case _:
+ max_score = len(questions)
+
+ return total_score, max_score
+
+
+def get_supervisor_attempts(supervisor, trainees=None):
+ from django.contrib.contenttypes.models import ContentType
+ from generic.models import CidUserExam
+ from atlas.models import CaseCollection
+ from django.urls import reverse
+ from django.utils import timezone
+
+ if trainees is None:
+ trainees = User.objects.filter(userprofile__supervisor=supervisor)
+
+ attempts = CidUserExam.objects.filter(user_user__in=trainees).select_related("user_user", "content_type")
+
+ parsed_attempts = []
+ for att in attempts:
+ exam = att.exam
+ if not exam:
+ continue
+
+ friendly_type = att.content_type.app_label
+ if friendly_type == "atlas":
+ friendly_type = "casecollection"
+
+ exam_supervisor_visible = getattr(exam, "results_supervisor_visible", False)
+ is_shared = att.share_with_supervisor or exam_supervisor_visible
+
+ score_str = "N/A"
+ detail_url = None
+
+ if is_shared:
+ if friendly_type == "casecollection":
+ total_cases = exam.casedetail_set.count()
+ reviewed_cases = att.selfreview_set.count()
+ score_str = f"{reviewed_cases} / {total_cases} cases"
+ detail_url = reverse(
+ "atlas:collection_history_user",
+ kwargs={
+ "exam_id": exam.pk,
+ "user_pk": att.user_user.pk,
+ },
+ )
+ else:
+ total_score, max_score = get_exam_attempt_score(friendly_type, exam, att.user_user)
+ if total_score is not None:
+ pct = int((total_score / max_score) * 100) if max_score > 0 else 0
+ score_str = f"{total_score} / {max_score} ({pct}%)"
+ else:
+ score_str = "No answers"
+ detail_url = reverse(
+ f"{friendly_type}:exam_scores_user_supervisor",
+ kwargs={"pk": exam.pk, "user_id": att.user_user.pk},
+ )
+ else:
+ score_str = "Private"
+
+ parsed_attempts.append({
+ "attempt": att,
+ "exam": exam,
+ "trainee": att.user_user,
+ "type": friendly_type,
+ "type_display": friendly_type.title() if friendly_type != "sbas" else "SBAs",
+ "date": att.end_time or att.start_time,
+ "is_shared": is_shared,
+ "score_str": score_str,
+ "detail_url": detail_url,
+ "completed": att.completed,
+ })
+ parsed_attempts.sort(key=lambda x: x["date"] or timezone.now(), reverse=True)
+ return parsed_attempts
+
+
def get_user_exams(user, supervisor_view=False):
EXAM_ANSWER_MAP = {
"physics": (PhysicsUserAnswer, PhysicsExam, "physics_exams"),
@@ -3547,11 +3667,34 @@ class ExamViews(View, LoginRequiredMixin):
return self.exam_scores_cid_user(request, pk, user=user)
def exam_scores_user_supervisor(self, request, pk, user_id):
+ from django.contrib.contenttypes.models import ContentType
+ from generic.models import CidUserExam
+
user = User.objects.get(id=user_id)
if not request.user.is_superuser:
- if not request.user.supervisor == user.userprofile.supervisor:
+ try:
+ if not request.user.supervisor == user.userprofile.supervisor:
+ raise PermissionDenied
+ except Exception:
raise PermissionDenied
+ exam = get_object_or_404(self.Exam, pk=pk)
+ ct = ContentType.objects.get_for_model(self.Exam)
+ cid_user_exam = CidUserExam.objects.filter(
+ content_type=ct,
+ object_id=exam.pk,
+ user_user=user
+ ).first()
+
+ is_visible = False
+ if exam.results_supervisor_visible:
+ is_visible = True
+ elif cid_user_exam and cid_user_exam.share_with_supervisor:
+ is_visible = True
+
+ if not is_visible:
+ raise PermissionDenied("This exam attempt has not been shared with the supervisor.")
+
return self.exam_scores_cid_user(
request, pk, user=user, supervisor=user.userprofile.supervisor.user
)
@@ -5988,27 +6131,26 @@ def create_user(request, context=None, trainee: bool = False):
class SupervisorDetail(DetailView):
model = Supervisor
- def get_queryset(self):
- """Allow supervisors to view their own detail page; managers see all."""
- qs = super().get_queryset()
- if not self.request.user.is_authenticated:
- return qs.none()
- if self.request.user.is_superuser or self.request.user.groups.filter(name="cid_user_manager").exists():
- return qs
- if hasattr(self.request.user, "supervisor"):
- return qs.filter(user=self.request.user)
- return qs.none()
-
def get_object(self, queryset=None):
- """Override to check permissions before returning object."""
+ """Return the supervisor if the requesting user is authorised.
+
+ - Superusers and ``cid_user_manager`` group members may view any supervisor.
+ - A supervisor linked to the requesting user may view their own record.
+ - All other authenticated users receive PermissionDenied (403).
+ - Unauthenticated users are redirected to the login page by
+ ``LoginRequiredMixin`` (enforced via the URLconf decorator or mixin).
+ """
obj = super().get_object(queryset)
- if not (
+ if not self.request.user.is_authenticated:
+ from django.contrib.auth.views import redirect_to_login
+ return redirect_to_login(self.request.get_full_path())
+ if (
self.request.user.is_superuser
or self.request.user.groups.filter(name="cid_user_manager").exists()
or (hasattr(self.request.user, "supervisor") and obj.user == self.request.user)
):
- raise PermissionDenied
- return obj
+ return obj
+ raise PermissionDenied
@user_is_cid_user_manager
@@ -6529,12 +6671,38 @@ def supervisor_overview(request, pk):
except User.supervisor.RelatedObjectDoesNotExist:
raise PermissionDenied()
- trainees = User.objects.filter(userprofile__supervisor=supervisor)
+ trainees = User.objects.filter(userprofile__supervisor=supervisor).select_related("userprofile__grade", "userprofile__site")
+ attempts = get_supervisor_attempts(supervisor, trainees)
+
+ # Calculate statistics for trainees
+ trainee_stats = []
+ for trainee in trainees:
+ trainee_attempts = [a for a in attempts if a["trainee"] == trainee]
+ total_count = len(trainee_attempts)
+ shared_count = sum(1 for a in trainee_attempts if a["is_shared"])
+ private_count = total_count - shared_count
+
+ last_active = None
+ if trainee_attempts:
+ last_active = trainee_attempts[0]["date"]
+
+ trainee_stats.append({
+ "trainee": trainee,
+ "total_count": total_count,
+ "shared_count": shared_count,
+ "private_count": private_count,
+ "last_active": last_active,
+ })
return render(
request,
"generic/supervisor_overview.html",
- {"supervisor": supervisor, "trainees": trainees},
+ {
+ "supervisor": supervisor,
+ "trainees": trainees,
+ "trainee_stats": trainee_stats,
+ "attempts": attempts,
+ },
)
@@ -6549,15 +6717,72 @@ def supervisor_trainee(request, pk, trainee_id):
except User.supervisor.RelatedObjectDoesNotExist:
raise PermissionDenied()
- # We do this to check that the supervisor can (should) see the trainee results
trainee = User.objects.get(userprofile__supervisor=supervisor, pk=trainee_id)
-
- exams = get_user_exams(trainee, supervisor_view=True)
+ attempts = get_supervisor_attempts(supervisor, [trainee])
return render(
request,
"generic/supervisor_trainee.html",
- {"supervisor": supervisor, "trainee": trainee, "all_exams": exams},
+ {
+ "supervisor": supervisor,
+ "trainee": trainee,
+ "attempts": attempts,
+ },
+ )
+
+
+@login_required
+def supervisor_collection_feedback(request, pk, trainee_id, collection_id):
+ from atlas.models import CaseCollection, CaseDetail
+ from atlas.views import _build_collection_feedback_rows
+ from django.contrib.contenttypes.models import ContentType
+
+ supervisor = get_object_or_404(Supervisor, pk=pk)
+
+ if not request.user.is_superuser:
+ try:
+ if not request.user.supervisor == supervisor:
+ raise PermissionDenied()
+ except User.supervisor.RelatedObjectDoesNotExist:
+ raise PermissionDenied()
+
+ trainee = get_object_or_404(User, userprofile__supervisor=supervisor, pk=trainee_id)
+ collection = get_object_or_404(CaseCollection, pk=collection_id)
+
+ ct = ContentType.objects.get_for_model(CaseCollection)
+ cid_user_exam = get_object_or_404(
+ CidUserExam, user_user=trainee, content_type=ct, object_id=collection.pk
+ )
+
+ if not (cid_user_exam.share_with_supervisor or collection.results_supervisor_visible):
+ raise PermissionDenied("This collection attempt has not been shared with the supervisor.")
+
+ casedetails = list(
+ CaseDetail.objects.filter(collection=collection).select_related("case").order_by("sort_order")
+ )
+ feedback_rows, outstanding_case_count, total_outstanding_feedback = _build_collection_feedback_rows(
+ collection=collection,
+ cid_user_exam=cid_user_exam,
+ casedetails=casedetails,
+ )
+
+ outstanding_rows = [row for row in feedback_rows if row["stats"].get("has_outstanding_feedback")]
+ history_rows = [row for row in feedback_rows if row["stats"].get("has_messages") and not row["stats"].get("has_outstanding_feedback")]
+
+ return render(
+ request,
+ "generic/supervisor_collection_feedback.html",
+ {
+ "supervisor": supervisor,
+ "trainee": trainee,
+ "collection": collection,
+ "cid_user_exam": cid_user_exam,
+ "feedback_rows": feedback_rows,
+ "outstanding_rows": outstanding_rows,
+ "history_rows": history_rows,
+ "outstanding_case_count": outstanding_case_count,
+ "total_outstanding_feedback": total_outstanding_feedback,
+ },
)
diff --git a/templates/base.html b/templates/base.html
index 650371cb..48617f96 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -314,6 +314,11 @@
People
{% endif %}
+ {% if request.user.supervisor %}
+
+ Supervisor
+
+ {% endif %}
Profile
diff --git a/templates/index.html b/templates/index.html
index e5ffc93c..a35df43a 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -43,6 +43,16 @@
{% endif %}
+ {% if request.user.supervisor %}
+