lost track of changes...

This commit is contained in:
Ross
2023-01-09 09:52:09 +00:00
parent 9fc457c8a5
commit af52658e44
28 changed files with 469 additions and 118 deletions
@@ -0,0 +1,40 @@
# Generated by Django 4.1.4 on 2022-12-19 13:30
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
('generic', '0047_auto_20221121_1637'),
]
operations = [
migrations.AlterField(
model_name='userusergroup',
name='archive',
field=models.BooleanField(default=False, help_text='Archived groups remain on the test system but are not displayed by default'),
),
migrations.AlterField(
model_name='userusergroup',
name='name',
field=models.CharField(blank=True, help_text='Name of the User Group', max_length=50),
),
migrations.CreateModel(
name='ExamUserStatus',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datetime', models.DateTimeField(auto_now_add=True)),
('status', models.CharField(max_length=255)),
('extra', models.CharField(max_length=255)),
('object_id', models.PositiveIntegerField()),
('cid_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='generic.ciduser')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('user_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
),
]
+40 -5
View File
@@ -184,8 +184,11 @@ class ExamBase(models.Model):
def get_exam_name(self):
return self.name
def get_json_url(self):
return reverse("{}:exam_json".format(self.app_name), args=(self.pk,))
def get_json_url(self, cid=None, passcode=None):
if cid is None:
return reverse("{}:exam_json".format(self.app_name), args=(self.pk,))
else:
return reverse("{}:exam_json_cid".format(self.app_name), args=(self.pk,cid, passcode))
def get_question_index(self, question):
return list(self.exam_questions.all()).index(question)
@@ -200,17 +203,24 @@ class ExamBase(models.Model):
authors = [i for i in self.author.all()]
return authors
def check_cid_user(self, cid, passcode, request=None, user_id=None):
def check_cid_user(self, cid: int|None, passcode: str|None, request: HttpRequest|None=None, user_id: int|None=None, allow_authors: bool=True):
if request is not None and request.user.is_superuser:
return True
if not self.valid_cid_users.exists() and not self.valid_user_users.exists():
return True
if user_id is not None:
if allow_authors:
if self.author.filter(pk=user_id).exists():
return True
if not self.valid_cid_users.exists() and not self.valid_user_users.exists():
return False
# Start by checking if the logged in user can access
if user_id is not None:
if self.valid_user_users.filter(pk=user_id).exists():
return True
# Then test CID data
if self.valid_cid_users.exists():
cid_user = self.valid_cid_users.filter(cid=cid).first()
@@ -365,6 +375,24 @@ class ExamBase(models.Model):
#self.save()
return [True, ""]
class ExamUserStatus(models.Model):
datetime = models.DateTimeField(auto_now_add=True)
cid_user = models.ForeignKey("CidUser", blank=True, null=True, on_delete=models.SET_NULL)
user_user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL)
status = models.CharField(max_length=255)
extra = models.CharField(max_length=255)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
exam = GenericForeignKey("content_type", "object_id")
def __str__(self):
if self.cid_user:
user = self.cid_user.cid
else:
user = self.user_user.username
return f"{self.datetime}: {user} - {self.status} ({self.extra})"
class NoteType(models.Model):
note_type = models.CharField(max_length=200)
@@ -620,6 +648,13 @@ class CidUserExam(models.Model):
# TODO switch to json field?
results_emailed_status = models.CharField(max_length=255, blank=True)
def __str__(self) -> str:
if self.cid_user is None:
user = self.user_user.username
else:
user = self.cid_user.cid
return f"{user}: {self.start_time:%Y-%m-%d %H:%M} {self.end_time:%Y-%m-%d %H:%M}"
CID_GROUP_EXAMS = (
("SBAs", "sba_cid_user_groups"),
("Physics", "physics_cid_user_groups"),
+52 -30
View File
@@ -1,49 +1,71 @@
<div class="card text-white bg-dark">
<h3>CID candidates</h3>
{% if cid_users %}
<p>{{cid_user_count}} CID candidates.</p>
{% if cid_users %}
<p>{{cid_user_count}} CID candidates.</p>
<ol>
{% for cid in cid_users %}
<ol>
{% for cid in cid_users %}
<li><a href="{% url 'generic:update_cid' cid.pk %}">{{cid.cid}}</a> [{{cid.passcode}}] {{cid.name}} /
Email: {{cid.email}}
{% if cid.supervisor %} / Supervisor email: {{cid.supervisor.email}}{% endif %} <br />
Internal candidate: {{cid.internal_candidate}}
{% if cid.login_email_sent %} / Login email sent{% endif %}
{% if cid.results_email_sent %} / Results email sent{% endif %}
</li>
<li><a href="{% url 'generic:update_cid' cid.pk %}">{{cid.cid}}</a> [{{cid.passcode}}] {{cid.name}} /
Email: {{cid.email}}
{% if cid.supervisor %} / Supervisor email: {{cid.supervisor.email}}{% endif %} <br />
Internal candidate: {{cid.internal_candidate}}
{% if cid.login_email_sent %} / Login email sent{% endif %}
{% if cid.results_email_sent %} / Results email sent{% endif %}
</li>
{% endfor %}
</ol>
{% else %}
<p>This exam has no CID candidates. Add some with the below link.</p>
{% endif %}
{% endfor %}
</ol>
{% else %}
<p>This exam has no CID candidates. Add some with the below link.</p>
{% endif %}
<a href="{% url exam.app_name|add:':exam_cids_edit' exam.pk %}">Edit Exam CIDs</a>
<a href="{% url exam.app_name|add:':exam_cids_edit' exam.pk %}">Edit Exam CIDs</a>
</div>
<br/>
<div class="card text-white bg-dark">
<h3>User candidates</h3>
{% if user_users %}
<p>{{user_user_count}} User candidates.</p>
{% if user_users %}
<p>{{user_user_count}} User candidates.</p>
<ol>
{% for user in user_users %}
<ol>
{% for user in user_users %}
<li>{{user.username}}
Email: {{user.email}}
</li>
<li>{{user.username}}
Email: {{user.email}}
</li>
{% endfor %}
</ol>
{% else %}
<p>This exam has no User candidates. Add some with the below link.</p>
{% endif %}
<a href="{% url exam.app_name|add:':exam_users_edit' exam.pk %}">Edit Exam Users</a>
</div>
<br/>
{% if user_exam_data %}
<div class="card text-white bg-dark">
<h3>Submitted candidates</h3>
<ul>
{% for user_data in user_exam_data %}
<li>
CID: {{user_data.cid_user}} /
User: {{user_data.user_user}} /
Start time: {{user_data.start_time}} /
End time: {{user_data.end_time}} /
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</ol>
{% else %}
<p>This exam has no User candidates. Add some with the below link.</p>
{% endif %}
<a href="{% url exam.app_name|add:':exam_users_edit' exam.pk %}">Edit Exam Users</a>
</div>
+1 -1
View File
@@ -18,7 +18,7 @@
{% if app_name in "rapids longs anatomy" %}
{% if request.user.is_staff %}<a href="{% url app_name|add:':mark_overview' pk=exam.pk %}">Mark answers</a>{% endif %}
{% endif %}
{% if request.user.is_staff %}<a href="{% url app_name|add:':exam_scores_cid' pk=exam.pk %}">Scores</a>{% endif %}
{% if request.user.is_staff %}<a href="{% url app_name|add:':exam_scores_all' pk=exam.pk %}">Scores</a>{% endif %}
{% endif %}
</div>
{% endfor %}
+54 -14
View File
@@ -8,6 +8,22 @@
User answer scores are cached, if you update or change an answer you will need to <a href="{% url exam.app_name|add:':exam_scores_refresh' exam.pk %}">refresh the scores</a>.
{% endif %}
{% if missing_cids %}
<div class="alert alert-warning" role="alert">
The following CIDs results have not been submitted:
{% for cid in missing_cids %}
{{cid}},
{% endfor %}
</div>
{% endif %}
{% if missing_users %}
<div class="alert alert-warning" role="alert">
The following users results have not been submitted:
{% for user_id in missing_users %}
{{user_id}},
{% endfor %}
</div>
{% endif %}
{% if unmarked %}
<div class="alert alert-warning" role="alert">
The following questions need marking
@@ -15,13 +31,13 @@
<a href="{% url exam.app_name|add:':mark' exam.pk exam_index %}">{{ exam_index|add:1 }}</a>
{% endfor %}
</div>
{% endif %}
</div>
<div id="stats-block">
<h3>Stats</h3>
Candidates: {{cids|length}}<br />
Questions: <span id="question-number">{{question_number}}</span><br />
Max score: {{max_score}}<br />
Mean: {{mean}}, Median {{median}}, Mode {{mode}}
@@ -34,7 +50,7 @@
<th>Normalised Score</th>
</tr>
{% for cid in cids %}
<tr>
<tr class="candidate-row" data-answer-count={{user_answer_count|get_item:cid}}>
{% if cid|slice:":1" == "u" %}
<td>{{cids_user_id_map|get_item:cid}}</td>
{% else %}
@@ -90,22 +106,22 @@
<details>
<summary>Email results</summary>
User results emailed: {{exam.exam_results_emailed|default:"Never"}}<br/>
<button id="email-results-button"
title="Email results to users and their supervisor"
hx-get="{% url exam.app_name|add:':exam_report_email' exam_id=exam.pk %}"
hx-target="#user-details"
hx-confirm="This will email results, please make sure scores have been refreshed before continuing"
hx-prompt="Please enter an additional email to send to (user and supervisor will automatically be used if available) if required"
<button id="email-results-button"
title="Email results to users and their supervisor"
hx-get="{% url exam.app_name|add:':exam_report_email' exam_id=exam.pk %}"
hx-target="#user-details"
hx-confirm="This will email results, please make sure scores have been refreshed before continuing"
hx-prompt="Please enter an additional email to send to (user and supervisor will automatically be used if available) if required"
>Email user results</button>
<button id="email-unsent-results-button" title="Email results to users and their supervisor"
hx-get="{% url exam.app_name|add:':exam_report_email_unsent' exam_id=exam.pk %}"
hx-target="#user-details"
hx-confirm="This will email results, please make sure scores have been refreshed before continuing"
hx-prompt="Please enter an additional email to send to (user and supervisor will automatically be used if available) if required"
hx-get="{% url exam.app_name|add:':exam_report_email_unsent' exam_id=exam.pk %}"
hx-target="#user-details"
hx-confirm="This will email results, please make sure scores have been refreshed before continuing"
hx-prompt="Please enter an additional email to send to (user and supervisor will automatically be used if available) if required"
>Email unsent user results</button>
<button id="email-results-check-button" title="Check the status of emailed results"
hx-get="{% url exam.app_name|add:':exam_report_email_status' exam_id=exam.pk %}"
hx-target="#user-details">Check email status</button>
hx-get="{% url exam.app_name|add:':exam_report_email_status' exam_id=exam.pk %}"
hx-target="#user-details">Check email status</button>
<p>Note: currently only works with registered users</p>
<div id="user-details"></div>
</details>
@@ -113,3 +129,27 @@
</div>
{% endblock %}
{% block js %}
<script>
$(document).ready(() => {
let question_number = document.getElementById("question-number").text;
$(".candidate-row").each((index, el) => {
console.log(index, el)
if (el.dataset.answerCount != question_number) {
$(el).addClass("missing-answers")
}
});
});
</script>
<style>
.missing-answers td:first-child:before {
content: "*";
color: red;
}
</style>
{% endblock %}
+6
View File
@@ -27,5 +27,11 @@
<a href="{% url 'accounts_bulk_create' %}">Bulk create users</a>
<a href="{% url 'create_user' %}">Create single user</a>
<details>
<summary>
Bulk edit
</summary>
<button>Delete supervisors</button>
</details>
{% endblock %}
+15 -4
View File
@@ -181,13 +181,13 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
),
path(
"exam/<int:pk>/scores",
generic_exam_view.exam_scores_cid,
name="exam_scores_cid",
generic_exam_view.exam_scores_all,
name="exam_scores_all",
),
path(
"exam/<int:pk>/scores/refresh",
generic_exam_view.exam_scores_refresh,
# cache_page(60 * 1)(exam_scores_cid),
# cache_page(60 * 1)(exam_scores_all),
name="exam_scores_refresh",
),
path(
@@ -217,7 +217,7 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
),
path(
"exam/submit",
generic_exam_view.postExamAnswers,
generic_exam_view.post_exam_answers,
name="exam_answers_submit",
),
path("exam/", generic_exam_view.exam_list, name="exam_list"),
@@ -235,11 +235,17 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
),
path("exam/json/", generic_exam_view.active_exams, name="active_exams"),
path("exam/json/<int:pk>", generic_exam_view.exam_json, name="exam_json"),
path("exam/json/<int:pk>/<int:cid>/<str:passcode>", generic_exam_view.exam_json_cid, name="exam_json_cid"),
path(
"exam/json/<int:pk>/unbased",
generic_exam_view.exam_json_unbased,
name="exam_json_unbased",
),
path(
"exam/json/<int:pk>/<int:cid>/<str:passcode>/unbased",
generic_exam_view.exam_json_cid_unbased,
name="exam_json_cid_unbased",
),
path(
"exam/json/<int:pk>/<int:sk>",
generic_exam_view.exam_question_json,
@@ -255,5 +261,10 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
generic_exam_view.exam_json_recreate,
name="exam_json_recreate",
),
path(
"exam/<int:pk>/user_status",
generic_exam_view.exam_user_status,
name="exam_user_status",
),
]
return urlpatterns
+150 -35
View File
@@ -17,7 +17,7 @@ from django.utils.html import format_html
from django.views.decorators.csrf import csrf_exempt
from django.core.exceptions import PermissionDenied, FieldError
from django.http import Http404, JsonResponse
from django.http import Http404, HttpRequest, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
@@ -58,7 +58,7 @@ from .forms import (
UserUserGroupForm,
)
from .models import CidUser, CidUserGroup, ExamBase, Examination, QuestionNote, Supervisor, UserGrades, UserProfile, UserUserGroup, get_next_cid
from .models import CidUser, CidUserGroup, ExamBase, ExamUserStatus, Examination, QuestionNote, Supervisor, UserGrades, UserProfile, UserUserGroup, get_next_cid
from rapids.models import Rapid as RapidQuestion
from rapids.models import Exam as RapidExam
@@ -87,6 +87,9 @@ from django.db.models import Prefetch
class AuthorRequiredMixin(object):
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
if self.request.user.is_superuser:
return obj
if self.request.user not in obj.author.all():
raise PermissionDenied() # or Http404
return obj
@@ -861,12 +864,24 @@ class ExamViews(View, LoginRequiredMixin):
"simple_content": format_html(
"""Answer scores updated <br/>
<a href='{}'>Return</a>""",
reverse(f"{self.app_name}:exam_scores_cid", kwargs={"pk": exam.pk}),
reverse(f"{self.app_name}:exam_scores_all", kwargs={"pk": exam.pk}),
)
},
)
def exam_cids(self, request, exam_id):
"""View that displays an overview of users that have been added to the exam.
Args:
request (_type_): _description_
exam_id (_type_): _description_
Raises:
PermissionDenied: _description_
Returns:
_type_: _description_
"""
exam = get_object_or_404(self.Exam, pk=exam_id)
# if not request.user.groups.filter(name="cid_user_manager").exists():
@@ -874,8 +889,7 @@ class ExamViews(View, LoginRequiredMixin):
if request.user not in exam.author.all():
if not request.user.is_superuser:
if not self.check_user_access(request.user, exam_id):
raise PermissionDenied
raise PermissionDenied
cid_users = exam.valid_cid_users.all()
cid_user_count = cid_users.count()
@@ -883,10 +897,13 @@ class ExamViews(View, LoginRequiredMixin):
user_users = exam.valid_user_users.all()
user_user_count = user_users.count()
user_exam_data = exam.cid_users.all()
context = {
"exam": exam,
"cid_users": cid_users,
"cid_user_count": cid_user_count,
"user_exam_data": user_exam_data,
"user_users": user_users,
"user_user_count": user_user_count,
"app_name": self.app_name,
@@ -903,8 +920,7 @@ class ExamViews(View, LoginRequiredMixin):
if not request.user.groups.filter(name="cid_user_manager").exists():
# raise PermissionDenied
if request.user not in exam.author.all():
if not self.check_user_access(request.user, exam_id):
raise PermissionDenied
raise PermissionDenied
current_user_users = exam.valid_user_users.all()
@@ -937,8 +953,7 @@ class ExamViews(View, LoginRequiredMixin):
if not request.user.groups.filter(name="cid_user_manager").exists():
# raise PermissionDenied
if request.user not in exam.author.all():
if not self.check_user_access(request.user, exam_id):
raise PermissionDenied
raise PermissionDenied
current_cid_users = exam.valid_cid_users.all()
@@ -980,6 +995,29 @@ class ExamViews(View, LoginRequiredMixin):
return notes
def exam_user_status(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
# Restrict just to exam authors
if request.user not in exam.author.all():
raise PermissionDenied
statuses = exam.exam_user_status.all()
return render(request, "exam_user_status.html", {"exam": exam, "statuses": statuses})
#if not self.check_user_access(request.user, pk):
# raise PermissionDenied
#q, content_type = get_question_and_content_type(self.question_type)
## Get active notes for type
#statuses = ExamUserStatus.objects.filter(
# content_type=content_type, object_id__in=pk
#)
#return statuses
@method_decorator(login_required)
def exam_json_edit(self, request, pk):
if request.method == "POST":
@@ -1252,7 +1290,7 @@ class ExamViews(View, LoginRequiredMixin):
},
)
def active_exams(self, request, json=True, based=True, cid=None, passcode=None):
def active_exams(self, request: HttpRequest, json: bool=True, based: bool=True, cid: int=None, passcode: str=None):
exams = self.Exam.objects.filter(archive=False)
active_exams = {"exams": []}
@@ -1264,11 +1302,12 @@ class ExamViews(View, LoginRequiredMixin):
return active_exams["exams"]
return JsonResponse({"status": "invalid"}, status=401)
exam: ExamBase
for exam in exams:
if exam.active or self.check_user_access(request.user, exam.pk):
print(exam.name, cid, passcode)
print(f"{exam.app_name=}, {exam.name=}, {cid=}, {passcode=}, {request.user=}")
if exam.exam_mode and not exam.check_cid_user(
cid, passcode, user_id=request.user.pk
cid, passcode, request=request, user_id=request.user.pk
):
print(exam.name, "fail")
continue
@@ -1278,7 +1317,7 @@ class ExamViews(View, LoginRequiredMixin):
else:
creation_time = "None"
url = request.build_absolute_uri(exam.get_json_url())
url = request.build_absolute_uri(exam.get_json_url(cid=cid, passcode=passcode))
# hacky
if not based:
url = url + "/unbased"
@@ -1310,7 +1349,7 @@ class ExamViews(View, LoginRequiredMixin):
return JsonResponse(active_exams)
@method_decorator(csrf_exempt)
def postExamAnswers(self, request):
def post_exam_answers(self, request):
if request.method == "POST":
n = 0
@@ -1486,32 +1525,42 @@ class ExamViews(View, LoginRequiredMixin):
return JsonResponse({"success": True, "question_count": n})
return JsonResponse({"success": False, "error": "Invalid data"})
def exam_json_unbased(self, request, pk):
"""
No (file based) caching is enabled for unbased exams
"""
exam = get_object_or_404(self.Exam, pk=pk)
def exam_json_cid(self, request, pk, cid, passcode):
return self.exam_json(request, pk, cid, passcode)
def exam_json_cid_unbased(self, request, pk, cid, passcode):
return self.exam_json_unbased(request, pk, cid, passcode)
def exam_json(self, request, pk, cid=None, passcode=None):
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
# Check access (for logged in users when inactive)
if not exam.active and not self.check_user_access(request.user, pk):
raise Http404("No available exam")
time = datetime.now()
# TODO: check access for logged in users
exam_json = exam.get_exam_json(based=False)
exam_json["generated"] = time.isoformat()
exam_json["exam_json_id"] = exam.exam_json_id
# exam_json["exam_active"] = False
return JsonResponse(exam_json)
def exam_json(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
if not exam.active and not self.check_user_access(request.user, pk):
# Check access for users
if request.user.is_anonymous:
user_id = None
else:
user_id = request.user.pk
if not exam.check_cid_user(cid, passcode, request, user_id):
raise Http404("No available exam")
# exam_json_cache = cache.get("{}_exam_json_{}".format(self.app_name, pk))
# Log exam access
user = request.user
if cid is not None:
user = None
cid_user = CidUser.objects.get(cid=cid)
else:
cid_user = None
exam.exam_user_status.create(cid_user=cid_user, user_user=user, status="downloaded")
path = "{0}{1}/exam/{2}.json".format(settings.MEDIA_ROOT, self.app_name, pk)
url = "{0}{1}/exam/{2}.json".format(settings.MEDIA_URL, self.app_name, pk)
@@ -1552,6 +1601,32 @@ class ExamViews(View, LoginRequiredMixin):
return JsonResponse(exam_json)
def exam_json_unbased(self, request, pk, cid=None, passcode=None):
"""
No (file based) caching is enabled for unbased exams
"""
exam = get_object_or_404(self.Exam, pk=pk)
if not exam.active and not self.check_user_access(request.user, pk):
raise Http404("No available exam")
time = datetime.now()
exam_json = exam.get_exam_json(based=False)
exam_json["generated"] = time.isoformat()
exam_json["exam_json_id"] = exam.exam_json_id
# exam_json["exam_active"] = False
# Log exam access
user = request.user
if cid is not None:
user = None
cid_user = CidUser.objects.get(cid=cid)
else:
cid_user = None
exam.exam_user_status.create(cid_user=cid, user_user=user, status="downloaded", extra="unbased")
return JsonResponse(exam_json)
def exam_question_json(self, request, pk, sk):
question = get_object_or_404(self.Question, pk=sk)
exam = get_object_or_404(self.Exam, pk=pk)
@@ -1687,12 +1762,29 @@ class ExamViews(View, LoginRequiredMixin):
template_context,
)
def exam_scores_cid(self, request, pk):
def exam_scores_all(self, request, pk):
"""The exam scores pages. Displays all user scores in a tabular format.
Args:
request (_type_): _description_
pk (_type_): _description_
Raises:
Http404: _description_
PermissionDenied: _description_
Returns:
_type_: _description_
"""
exam = get_object_or_404(self.Exam, pk=pk)
if not exam.exam_mode:
raise Http404("Packet not in exam mode")
# Restrict access to the scores page just to exam authors
if request.user not in exam.author.all():
raise PermissionDenied
# user_answers_and_marks = defaultdict(list)
user_answers_marks = defaultdict(list)
user_answers = defaultdict(list)
@@ -1704,6 +1796,10 @@ class ExamViews(View, LoginRequiredMixin):
cached_scores = True
valid_cid_users = set(exam.valid_cid_users.all().values_list("cid", flat=True))
valid_user_users = set(exam.valid_user_users.all().values_list("pk", flat=True))
if self.app_name in ("rapids"):
user_answers_callstates = defaultdict(list)
user_answers_callstates_counted = {}
@@ -1719,8 +1815,13 @@ class ExamViews(View, LoginRequiredMixin):
question__in=questions, exam__id=pk
)
question_number = questions.count()
cids = set()
plain_cids = set()
user_ids = set()
cids_user_id_map = {}
# Loop through all candidates
@@ -1731,6 +1832,7 @@ class ExamViews(View, LoginRequiredMixin):
cids_user_id_map[cid] = cid
# cid_passcodes[cid] = cid_user_answer.passcode
cids.add(cid)
plain_cids.add(cid_user_answer.cid)
else:
cid = f"u/{cid_user_answer.user.pk}"
# cids_user_id_map[cid] = cid_user_answer.user.username
@@ -1739,6 +1841,7 @@ class ExamViews(View, LoginRequiredMixin):
name = cid_user_answer.user.username
cids_user_id_map[cid] = name
cids.add(cid)
user_ids.add(cid_user_answer.user.pk)
s = cid_user_answer
# user_names[cid] = cid
@@ -1773,6 +1876,7 @@ class ExamViews(View, LoginRequiredMixin):
user_scores = {}
user_scores_normalised = {}
user_answer_count = {}
for user in user_answers_marks:
if self.app_name in ("rapids", "anatomy", "sbas"):
@@ -1791,15 +1895,17 @@ class ExamViews(View, LoginRequiredMixin):
else:
user_scores_normalised[user] = user_scores[user]
user_answer_count[user] = len(user_answers_marks[user])
# ignore scores of 0 for stats
user_scores_list = [i for i in user_scores.values() if i > 0]
if self.app_name in ("rapids", "anatomy"):
max_score = len(questions) * 2
max_score = question_number * 2
elif self.app_name == "physics":
max_score = len(questions) * 5
max_score = question_number * 5
else:
max_score = len(questions)
max_score = question_number
if len(user_scores_list) < 1:
mean = 0
@@ -1856,10 +1962,13 @@ class ExamViews(View, LoginRequiredMixin):
template_variables = {
"cids": sorted(cids),
"missing_cids": valid_cid_users - plain_cids,
"missing_users": valid_user_users - user_ids,
# "cid_passcodes": cid_passcodes,
"exam": exam,
"unmarked": unmarked,
"questions": questions,
"question_number": question_number,
"by_question": by_question,
# "user_answers": dict(user_answers),
# "user_answers_marks": dict(user_answers_marks),
@@ -1868,6 +1977,7 @@ class ExamViews(View, LoginRequiredMixin):
# "user_scores_list": user_scores_list,
# "user_names": user_names,
# "user_answers_and_marks": user_answers_and_marks,
"user_answer_count": user_answer_count,
"cids_user_id_map": cids_user_id_map,
"max_score": max_score,
"mean": mean,
@@ -2001,15 +2111,20 @@ class ExamCloneMixin:
old_object = get_object_or_404(self.model, pk=self.kwargs["exam_id"])
initial_data = model_to_dict(old_object, exclude=["id"])
# We manually transfer the forign keys / m2m relationships
questions = old_object.exam_questions.all().values_list("id", flat=True)
authors = old_object.author.all().values_list("id", flat=True)
self.exam_questions = list(questions)
self.author = list(authors)
return initial_data
def form_valid(self, form):
object = form.save()
# Reapply these otherwise they get lost?
object.exam_questions.set(self.exam_questions)
object.author.set(self.author)
object.save()
return HttpResponseRedirect(object.get_absolute_url())