diff --git a/anatomy/templates/anatomy/exam_cids.html b/anatomy/templates/anatomy/exam_cids.html
deleted file mode 100644
index fea07897..00000000
--- a/anatomy/templates/anatomy/exam_cids.html
+++ /dev/null
@@ -1,21 +0,0 @@
-{% extends 'anatomy/exams.html' %}
-
-{% block content %}
-
- {% load thumbnail %}
-
-
Exam: {{ exam.name }}
-
-
{{cid_user_count}} users.
-
-
-
- {% for cid in cid_users %}
- - {{cid.cid}} [{{cid.passcode}}]
-
- {% endfor %}
-
-
-
-
-{% endblock %}
diff --git a/anatomy/templates/anatomy/exams.html b/anatomy/templates/anatomy/exams.html
index e053338b..0b659555 100644
--- a/anatomy/templates/anatomy/exams.html
+++ b/anatomy/templates/anatomy/exams.html
@@ -8,7 +8,7 @@
{% if exam.exam_mode %}
Mark /
Scores /
- CIDs /
+ Candidates /
{% endif %}
Add New Question
{% endblock %}
diff --git a/anatomy/views.py b/anatomy/views.py
index 816831e3..3ac27502 100644
--- a/anatomy/views.py
+++ b/anatomy/views.py
@@ -226,7 +226,7 @@ def loadJsonAnswer(answer):
# in the frontend this is appended with the type
# e.g. anatomy/1
- # TODO Update to only allow valid CIDs
+ # TODO Update to only allow valid Candidates
eid = int(answer["eid"].split("/")[1])
# As access is not restricted make sure the data appears valid
if (not isinstance(answer["cid"], int)) or (
diff --git a/generic/forms.py b/generic/forms.py
index 44ece2a5..20bd50ec 100755
--- a/generic/forms.py
+++ b/generic/forms.py
@@ -10,7 +10,7 @@ from django.forms import (
from django.forms import inlineformset_factory
from generic.models import (
- Examination, QuestionNote
+ CidUser, Examination, QuestionNote
)
from django.contrib.admin.widgets import FilteredSelectMultiple
@@ -66,3 +66,8 @@ class QuestionNoteForm(ModelForm):
##initial["content_id"] = pk
#self.fields["content_type"].initial = content_type
#self.fields["object_id"].initial = pk
+
+class CidUserForm(ModelForm):
+ class Meta:
+ model = CidUser
+ fields = ["cid", "passcode", "active", "internal_candidate", "name", "email", "supervisor_email"]
\ No newline at end of file
diff --git a/generic/models.py b/generic/models.py
index a54fdd03..2da0d104 100644
--- a/generic/models.py
+++ b/generic/models.py
@@ -1,8 +1,11 @@
+from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import models
from django.http import HttpRequest
from smtplib import SMTPException
from django.urls import reverse
+from django.views.generic.edit import CreateView
+from reversion.views import RevisionMixin
import tagulous
import tagulous.models
diff --git a/generic/tables.py b/generic/tables.py
index 80dd95af..df3c097b 100644
--- a/generic/tables.py
+++ b/generic/tables.py
@@ -23,7 +23,10 @@ class CidUserTable(tables.Table):
# edit = tables.LinkColumn(
# "anatomy:anatomy_question_update", text="Edit", args=[A("pk")], orderable=False
# )
- cid = tables.LinkColumn("cid_scores", args=[A("cid"), A("passcode")], orderable=True)
+ cid = tables.LinkColumn(
+ "cid_scores", args=[A("cid"), A("passcode")], orderable=True
+ )
+ edit = tables.LinkColumn("generic:update_cid", text="Edit", args=[A("pk")], orderable=True)
# image = ImageColumn("image", orderable=False)
# clone = tables.LinkColumn('anatomy:anatomy_question_clone',
# text='Clone',
diff --git a/generic/templates/generic/base.html b/generic/templates/generic/base.html
index 550e2ab1..bdbb5448 100755
--- a/generic/templates/generic/base.html
+++ b/generic/templates/generic/base.html
@@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% block title %}
-CIDs
+Candidates
{% endblock %}
{% block css %}
@@ -15,7 +15,7 @@ CIDs
{% block content %}
{% endblock %}
{% block navigation %}
-CIDs:
+Candidates:
{% if request.user.is_authenticated %}
Cids /
Groups /
diff --git a/generic/templates/generic/ciduser_form.html b/generic/templates/generic/ciduser_form.html
new file mode 100755
index 00000000..d13dae67
--- /dev/null
+++ b/generic/templates/generic/ciduser_form.html
@@ -0,0 +1,26 @@
+{% extends "atlas/base.html" %}
+
+
+{% block css %}
+{% endblock %}
+{% block js %}
+
+{{form.media}}
+
+
+
+
+{% endblock %}
+{% block content %}
+Edit CID User
+Use this form to create or edit a CID user.
+
+{% endblock %}
\ No newline at end of file
diff --git a/generic/urls.py b/generic/urls.py
index 3aff1b23..161bd44c 100755
--- a/generic/urls.py
+++ b/generic/urls.py
@@ -26,6 +26,7 @@ urlpatterns = [
),
name="examination-autocomplete",
),
+ path("cids/manage//update", views.CidUserUpdate.as_view(), name="update_cid"),
path("cids/manage/", views.CidUserView.as_view(), name="manage_cids"),
path("cids/group/", views.group_view, name="group_view"),
path("cids/group//email", views.group_email, name="group_email"),
diff --git a/generic/views.py b/generic/views.py
index c1a7b146..58b0eb1f 100644
--- a/generic/views.py
+++ b/generic/views.py
@@ -25,14 +25,17 @@ import json
import urllib
from django.views import View
+from django.views.generic.edit import CreateView, UpdateView
from django_filters.views import FilterView
from django_tables2.views import SingleTableMixin
+from reversion.views import RevisionMixin
from generic.decorators import user_is_cid_user_manager
from generic.filters import CidUserFilter
from generic.tables import CidUserTable
from .forms import (
+ CidUserForm,
ExaminationForm,
)
@@ -414,10 +417,20 @@ class ExamViews(View, LoginRequiredMixin):
if not self.check_user_access(request.user):
raise PermissionDenied
+ marking = False
+
+ if self.app_name in ("rapids", "anatomy", "longs"):
+ marking = True
+
return render(
request,
"exam_list.html".format(self.app_name),
- {"exams": exams, "app_name": self.app_name, "view_all": all},
+ {
+ "exams": exams,
+ "app_name": self.app_name,
+ "view_all": all,
+ "marking": marking,
+ },
)
@method_decorator(login_required)
@@ -526,8 +539,13 @@ class ExamViews(View, LoginRequiredMixin):
return render(
request,
- "{}/exam_cids.html".format(self.app_name),
- {"exam": exam, "cid_users": cid_users, "cid_user_count": cid_user_count},
+ "exam_cids.html",
+ {
+ "exam": exam,
+ "cid_users": cid_users,
+ "cid_user_count": cid_user_count,
+ "app_name": self.app_name,
+ },
)
# @method_decorator(login_required)
@@ -1287,7 +1305,7 @@ def group_email(request, pk, resend=False):
if resend:
users = group.ciduser_set.filter()
else:
- users = group.ciduser_set.filter(login_email_sent=False)
+ users = gGroup.ciduser_set.filter(login_email_sent=False)
sent = []
not_sent = []
@@ -1449,3 +1467,13 @@ def manage_cid_users(request):
return JsonResponse({"status": "success"})
return JsonResponse({"status": "fail"})
+
+
+class CidUserCreate(RevisionMixin, LoginRequiredMixin, CreateView):
+ model = CidUser
+ form_class = CidUserForm
+
+
+class CidUserUpdate(RevisionMixin, LoginRequiredMixin, UpdateView):
+ model = CidUser
+ form_class = CidUserForm
diff --git a/longs/templates/longs/exams.html b/longs/templates/longs/exams.html
index 4ac3011b..a265c7cd 100644
--- a/longs/templates/longs/exams.html
+++ b/longs/templates/longs/exams.html
@@ -1,7 +1,11 @@
{% extends 'longs/base.html' %}
{% block navigation %}
-{{block.super}}
-
-Exams: {{exam.name}}-> Overview / Mark / Scores
-{% endblock %}
\ No newline at end of file
+ {{block.super}}
+
+ Exams: {{exam.name}}->
+ Overview /
+ Mark /
+ Scores
+ Candidates /
+{% endblock %}
diff --git a/longs/urls.py b/longs/urls.py
index 29dcce98..ff349a6b 100755
--- a/longs/urls.py
+++ b/longs/urls.py
@@ -125,6 +125,7 @@ urlpatterns = [
path("exam/", views.GenericExamViews.exam_list, name="exam_list"),
path("exam/all", views.GenericExamViews.exam_list_all, name="exam_list_all"),
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
+ path("exam//cids", views.GenericExamViews.exam_cids, name="exam_cids"),
path("exam//clone", views.ExamClone.as_view(), name="exam_clone"),
path("exam//update", views.ExamUpdate.as_view(), name="exam_update"),
path("exam//delete", views.ExamDelete.as_view(), name="exam_delete"),
diff --git a/physics/urls.py b/physics/urls.py
index e170e4c5..30115f69 100644
--- a/physics/urls.py
+++ b/physics/urls.py
@@ -43,6 +43,7 @@ urlpatterns = [
name="exam_toggle_results_published",
),
path("exam//clone", views.ExamClone.as_view(), name="exam_clone"),
+ path("exam//cids", views.GenericExamViews.exam_cids, name="exam_cids"),
path("exam//update", views.ExamUpdate.as_view(), name="exam_update"),
path("exam//delete", views.ExamDelete.as_view(), name="exam_delete"),
path(
diff --git a/rapids/templates/rapids/exams.html b/rapids/templates/rapids/exams.html
index ab8c5f66..1d257c34 100644
--- a/rapids/templates/rapids/exams.html
+++ b/rapids/templates/rapids/exams.html
@@ -8,6 +8,7 @@
{% if exam.exam_mode %}
Mark /
Scores /
+ Candidates /
{% endif %}
Add New Question
{% endblock %}
diff --git a/rapids/urls.py b/rapids/urls.py
index faabb137..b480777e 100755
--- a/rapids/urls.py
+++ b/rapids/urls.py
@@ -105,6 +105,7 @@ urlpatterns = [
path("exam/all", views.GenericExamViews.exam_list_all, name="exam_list_all"),
path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
path("exam//clone", views.ExamClone.as_view(), name="exam_clone"),
+ path("exam//cids", views.GenericExamViews.exam_cids, name="exam_cids"),
path("exam//update", views.ExamUpdate.as_view(), name="exam_update"),
path("exam//delete", views.ExamDelete.as_view(), name="exam_delete"),
path("exam/json/", views.GenericExamViews.active_exams, name="active_exams"),
diff --git a/sbas/templates/sbas/exams.html b/sbas/templates/sbas/exams.html
index 8512644a..7e74a3a8 100644
--- a/sbas/templates/sbas/exams.html
+++ b/sbas/templates/sbas/exams.html
@@ -1,7 +1,10 @@
{% extends 'sbas/base.html' %}
{% block navigation %}
-{{block.super}}
-
-Exams: {{exam.name}}-> Overview / Scores
-{% endblock %}
\ No newline at end of file
+ {{block.super}}
+
+ Exams: {{exam.name}}->
+ Overview /
+ Scores
+ Candidates /
+{% endblock %}
diff --git a/sbas/urls.py b/sbas/urls.py
index 0e015375..555eed3c 100644
--- a/sbas/urls.py
+++ b/sbas/urls.py
@@ -48,6 +48,8 @@ urlpatterns = [
),
# path("exam/submit", views.GenericExamViews.postExamAnswers, name="exam_answers_submit"),
path("exam/", views.GenericExamViews.exam_list, name="exam_list"),
+ path("exam//clone", views.ExamClone.as_view(), name="exam_clone"),
+ path("exam//cids", views.GenericExamViews.exam_cids, name="exam_cids"),
path("exam/available", views.active_exams, name="active_exams"),
# path("exam/json/", views.exam_json, name="exam_json"),
# path("exam/json//recreate", views.exam_json_recreate, name="exam_json_recreate"),
diff --git a/sbas/views.py b/sbas/views.py
index be3f74c8..8f2e1693 100644
--- a/sbas/views.py
+++ b/sbas/views.py
@@ -1,6 +1,7 @@
+from reversion.views import RevisionMixin
from generic.mixins import SuperuserRequiredMixin
from django.views.generic.detail import DetailView
-from sbas.forms import CidUserAnswerForm
+from sbas.forms import CidUserAnswerForm, ExamForm
from django.shortcuts import render, get_object_or_404, redirect
from django.views.decorators.csrf import csrf_exempt
from django import forms
@@ -37,7 +38,7 @@ import json
import statistics
import plotly.express as px
-from generic.views import ExamViews, GenericViewBase
+from generic.views import ExamCloneMixin, ExamViews, GenericViewBase
from rest_framework import viewsets, permissions
from rest_framework.pagination import PageNumberPagination
@@ -319,3 +320,58 @@ class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
GenericViews = GenericViewBase("sbas", Question, CidUserAnswer, Exam)
+
+class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
+ model = Question
+ success_url = reverse_lazy("sbas:question_list")
+
+
+class ExamCreate(RevisionMixin, LoginRequiredMixin, CreateView):
+ model = Exam
+ form_class = ExamForm
+
+ def form_valid(self, form):
+ self.object = form.save(commit=False)
+ self.object.save()
+
+ form.instance.author.add(self.request.user.id)
+ return super().form_valid(form)
+
+
+class ExamUpdate(
+ RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView
+):
+ model = Exam
+ form_class = ExamForm
+
+ def form_valid(self, form):
+ self.object = form.save(commit=False)
+ self.object.save()
+
+ form.instance.author.add(self.request.user.id)
+ return super().form_valid(form)
+
+
+#class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
+# # queryset = Exam.objects.all().order_by('name')
+# serializer_class = ExamSerializer
+# permission_classes = [permissions.IsAuthenticated]
+#
+# def get_queryset(self):
+# """
+# This view should return a list exams available to a user.
+# """
+# user = self.request.user
+# if user.groups.filter(name="sbas_checker").exists():
+# return Exam.objects.all()
+#
+# return Exam.objects.filter(author__id=user.id)
+
+
+class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
+ model = Exam
+ template_name = "exam_confirm_delete.html"
+ success_url = reverse_lazy("sbas:index")
+
+class ExamClone(ExamCloneMixin, ExamCreate):
+ """Clone exam view"""
diff --git a/templates/exam_cids.html b/templates/exam_cids.html
new file mode 100644
index 00000000..ef0ca8f9
--- /dev/null
+++ b/templates/exam_cids.html
@@ -0,0 +1,27 @@
+{% extends app_name|add:'/exams.html' %}
+
+{% block content %}
+
+ {% load thumbnail %}
+
+
Exam: {{ exam.name }}
+
+
{{cid_user_count}} candidates.
+
+
+
+ {% for cid in cid_users %}
+
+ - {{cid.cid}} [{{cid.passcode}}] {{cid.name}} /
+ {{cid.email}} /
+ {{cid.supervisor_email}} /
+ {% if cid.login_email_sent %}Login email sent{% endif %} /
+ {% if cid.results_email_sent %}Results email sent{% endif %} /
+
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/templates/exam_list.html b/templates/exam_list.html
index cbcc9d9e..37418305 100644
--- a/templates/exam_list.html
+++ b/templates/exam_list.html
@@ -8,7 +8,11 @@
{% for exam in exams %}
{% if exam.active %}
- {{exam.name}} MarkScores Results Published
+ {{exam.name}}
+ {% if marking %}Mark{% endif %}
+ Candidates
+ Scores
+ Results Published
{% endif %}
{% endfor %}
@@ -19,7 +23,11 @@
{% for exam in exams %}
{% if not exam.active %}
- {{exam.name}} MarkScores Results Published
+ {{exam.name}}
+ {% if marking %}Mark{% endif %}
+ Candidates
+ Scores
+ Results Published
{% endif %}
{% endfor %}