.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.13 on 2022-06-26 15:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('generic', '0035_userprofile_grade'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ciduserexam',
|
||||
name='results_emailed_status',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
]
|
||||
+95
-7
@@ -145,6 +145,8 @@ class ExamBase(models.Model):
|
||||
# Is this actually used?
|
||||
cid_users = GenericRelation("generic.CidUserExam")
|
||||
|
||||
exam_results_emailed = models.DateTimeField(default=None, null=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -153,11 +155,14 @@ class ExamBase(models.Model):
|
||||
self.recreate_json = recreate_json
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_exam_stats(self):
|
||||
text = f"""{self.name}
|
||||
{"-"*len(self.name)}
|
||||
- Candidates: {int(self.stats_candidates)}
|
||||
def get_exam_stats(self, name=True):
|
||||
text = f"""- Candidates: {int(self.stats_candidates)}
|
||||
- Mean: {self.stats_mean:.2f} Median: {self.stats_median} Mode: {self.stats_mode} [Min: {self.stats_min} / Max: {self.stats_max}]
|
||||
"""
|
||||
if name:
|
||||
text = f"""{self.name}
|
||||
{"-"*len(self.name)}
|
||||
{text}
|
||||
"""
|
||||
return text
|
||||
|
||||
@@ -223,7 +228,7 @@ class ExamBase(models.Model):
|
||||
|
||||
if user_user is not None:
|
||||
c = CidUserExam.objects.filter(
|
||||
content_type=content_type, object_id=self.pk, user_user=cid_user
|
||||
content_type=content_type, object_id=self.pk, user_user=user_user
|
||||
).first()
|
||||
if c:
|
||||
return c
|
||||
@@ -270,12 +275,93 @@ class ExamBase(models.Model):
|
||||
)
|
||||
|
||||
def get_cid_user_score(self, cid_user):
|
||||
c = str(cid_user)
|
||||
c = "c/"+str(cid_user)
|
||||
if c in self.user_scores:
|
||||
return self.user_scores[c]
|
||||
else:
|
||||
return False
|
||||
|
||||
def generate_user_report(self, user):
|
||||
exam_text = [f"Candidate {user.first_name} [{user.email}]"]
|
||||
|
||||
exam_name = self.name
|
||||
|
||||
exam_text.append(f"{'='*len(exam_name)}\n{exam_name}\n{'='*len(exam_name)}")
|
||||
|
||||
try:
|
||||
user_score = self.user_scores["u/"+str(user.pk)]["score"]
|
||||
|
||||
if self.app_name == "rapids":
|
||||
callstate = self.user_scores["u/"+str(user.pk)]["callstates"]
|
||||
except KeyError:
|
||||
user_score = "Score not generated"
|
||||
|
||||
try:
|
||||
percentage = (
|
||||
f" ({user_score / int(self.stats_max_possible) * 100:.2f}%)"
|
||||
)
|
||||
except TypeError:
|
||||
percentage = ""
|
||||
|
||||
|
||||
exam_text.append(
|
||||
f"""Results\n{'-'*len('Results')}
|
||||
# Score: {user_score} / {int(self.stats_max_possible)}{percentage}
|
||||
"""
|
||||
)
|
||||
stats = self.get_exam_stats(name=False)
|
||||
exam_text.append(f"Stats\n{'-'*len('Stats')}\n{stats}")
|
||||
|
||||
|
||||
exam_text.append(
|
||||
f"Answers breakdown: {callstate}"
|
||||
)
|
||||
|
||||
msg = "\n\n".join(exam_text)
|
||||
|
||||
return msg
|
||||
|
||||
def email_user_results(self, user, resend=False, additional_emails=[]):
|
||||
print("email_user_reuslts:", user)
|
||||
|
||||
#if self.results_email_sent and not resend:
|
||||
# return False, "Already sent."
|
||||
|
||||
# Get a list of taken exams
|
||||
msg = self.generate_user_report(user)
|
||||
|
||||
print(msg)
|
||||
|
||||
if not user.email:
|
||||
return False, "User has no email"
|
||||
|
||||
emails = [user.email]
|
||||
|
||||
supervisor_email = user.userprofile.supervisor_email
|
||||
|
||||
if supervisor_email:
|
||||
emails.append(supervisor_email)
|
||||
|
||||
|
||||
if additional_emails:
|
||||
emails.extend(additional_emails)
|
||||
|
||||
try:
|
||||
send_mail(
|
||||
f"{self.name} results",
|
||||
msg,
|
||||
"no-reply@penracourses.org.uk",
|
||||
emails,
|
||||
fail_silently=False,
|
||||
# html_message=html_msg,
|
||||
)
|
||||
except SMTPException as e:
|
||||
return False, e
|
||||
|
||||
#self.results_email_sent = True
|
||||
#self.save()
|
||||
return True, ""
|
||||
|
||||
|
||||
class NoteType(models.Model):
|
||||
note_type = models.CharField(max_length=200)
|
||||
@@ -396,7 +482,7 @@ class CidUser(models.Model):
|
||||
|
||||
for exam in exams:
|
||||
try:
|
||||
user_score = exam.user_scores[str(self.cid)]
|
||||
user_score = exam.user_scores["c/"+str(self.cid)]
|
||||
except KeyError:
|
||||
user_score = "Score not generated"
|
||||
t = exam.get_exam_stats()
|
||||
@@ -523,6 +609,8 @@ class CidUserExam(models.Model):
|
||||
|
||||
manual_submission = models.BooleanField(default=False)
|
||||
|
||||
results_emailed_status = models.CharField(max_length=255, blank=True)
|
||||
|
||||
|
||||
class CidUserGroup(models.Model):
|
||||
name = models.CharField(blank=True, max_length=50)
|
||||
|
||||
+152
-33
@@ -1,4 +1,4 @@
|
||||
from collections import defaultdict
|
||||
from collections import defaultdict, Counter
|
||||
import statistics
|
||||
import threading
|
||||
from dal import autocomplete
|
||||
@@ -294,7 +294,9 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
# exam_group = group_map[self.app_name]
|
||||
|
||||
class ExamFilter(FilterSet):
|
||||
sort_order = OrderingFilter(fields=(("name", "name"), ("exam_mode", "exam_mode")))
|
||||
sort_order = OrderingFilter(
|
||||
fields=(("name", "name"), ("exam_mode", "exam_mode"))
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = exam
|
||||
@@ -529,16 +531,15 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
def exam_list(self, request, all=False):
|
||||
|
||||
if not self.check_user_access(request.user):
|
||||
#raise PermissionDenied
|
||||
exam_list = self.Exam.objects.filter(author__id=request.user.id).order_by("name")
|
||||
# raise PermissionDenied
|
||||
exam_list = self.Exam.objects.filter(author__id=request.user.id).order_by(
|
||||
"name"
|
||||
)
|
||||
else:
|
||||
exam_list = self.Exam.objects.all().order_by("name")
|
||||
|
||||
if not all:
|
||||
exams = exam_list.filter(exam_mode=True, archive=False).order_by(
|
||||
"name"
|
||||
)
|
||||
|
||||
exams = exam_list.filter(exam_mode=True, archive=False).order_by("name")
|
||||
|
||||
marking = False
|
||||
|
||||
@@ -556,6 +557,90 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
},
|
||||
)
|
||||
|
||||
@method_decorator(login_required)
|
||||
def exam_report_email_status(self, request, exam_id):
|
||||
exam = get_object_or_404(self.Exam.objects.prefetch_related("author"), pk=exam_id)
|
||||
|
||||
if request.user not in exam.author.all():
|
||||
if not self.check_user_access(request.user, exam_id):
|
||||
raise PermissionDenied
|
||||
|
||||
users = [i[2:] for i in exam.user_scores if i.startswith("u/")]
|
||||
|
||||
status = {}
|
||||
for u in users:
|
||||
user = User.objects.get(pk=u)
|
||||
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
||||
status[user.username] = user_exam.results_emailed_status
|
||||
|
||||
return JsonResponse(status)
|
||||
|
||||
|
||||
@method_decorator(login_required)
|
||||
def exam_report_email(self, request, exam_id):
|
||||
exam = get_object_or_404(self.Exam.objects.prefetch_related("author"), pk=exam_id)
|
||||
|
||||
if request.user not in exam.author.all():
|
||||
if not self.check_user_access(request.user, exam_id):
|
||||
raise PermissionDenied
|
||||
|
||||
|
||||
# We only need to send emails to those who have scores
|
||||
# (who should be in exam.user_scores)
|
||||
users = [i[2:] for i in exam.user_scores if i.startswith("u/")]
|
||||
|
||||
time = timezone.now()
|
||||
|
||||
email_results = {}
|
||||
for u in users:
|
||||
user = User.objects.get(pk=u)
|
||||
email_results[user.username] = exam.email_user_results(user)
|
||||
|
||||
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
|
||||
user_exam.results_emailed_status = json.dumps(email_results[user.username]) + f" {time}"
|
||||
user_exam.save()
|
||||
|
||||
exam.exam_results_emailed = time
|
||||
exam.save()
|
||||
|
||||
return JsonResponse(email_results)
|
||||
|
||||
@method_decorator(login_required)
|
||||
def exam_user_report_email(self, request, exam_id, user_id):
|
||||
# print(Exam.objects.all())
|
||||
# exams = Exam.objects.all()
|
||||
# print("test", Exam.objects.all().get(id=pk))
|
||||
exam = get_object_or_404(self.Exam.objects.prefetch_related("author"), pk=exam_id)
|
||||
|
||||
u = get_object_or_404(User, pk=user_id)
|
||||
|
||||
if request.user not in exam.author.all():
|
||||
if not self.check_user_access(request.user, exam_id):
|
||||
raise PermissionDenied
|
||||
|
||||
res = exam.email_user_results(u)
|
||||
|
||||
return HttpResponse(res)
|
||||
|
||||
@method_decorator(login_required)
|
||||
def exam_user_report(self, request, exam_id, user_id):
|
||||
# print(Exam.objects.all())
|
||||
# exams = Exam.objects.all()
|
||||
# print("test", Exam.objects.all().get(id=pk))
|
||||
exam = get_object_or_404(self.Exam.objects.prefetch_related("author"), pk=exam_id)
|
||||
|
||||
u = get_object_or_404(User, pk=user_id)
|
||||
|
||||
if request.user not in exam.author.all():
|
||||
if not self.check_user_access(request.user, exam_id):
|
||||
raise PermissionDenied
|
||||
|
||||
res = exam.generate_user_report(u)
|
||||
print(res)
|
||||
|
||||
return HttpResponse(res)
|
||||
|
||||
|
||||
@method_decorator(login_required)
|
||||
def exam_overview(self, request, pk):
|
||||
# print(Exam.objects.all())
|
||||
@@ -563,6 +648,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
# print("test", Exam.objects.all().get(id=pk))
|
||||
exam = get_object_or_404(self.Exam.objects.prefetch_related("author"), pk=pk)
|
||||
|
||||
|
||||
if request.user not in exam.author.all():
|
||||
if not self.check_user_access(request.user, pk):
|
||||
raise PermissionDenied
|
||||
@@ -1396,6 +1482,10 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
by_question = defaultdict(dict)
|
||||
unmarked = set()
|
||||
|
||||
if self.app_name in ("rapids"):
|
||||
user_answers_callstates = defaultdict(list)
|
||||
user_answers_callstates_counted = {}
|
||||
|
||||
if self.app_name in ("physics", "sbas"):
|
||||
questions = exam.exam_questions.all()
|
||||
else:
|
||||
@@ -1408,15 +1498,18 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
|
||||
cids = set()
|
||||
|
||||
cids_user_id_map = {}
|
||||
|
||||
# Loop through all candidates
|
||||
for cid_user_answer in cid_user_answers:
|
||||
# Convoluted (probably...)
|
||||
if cid_user_answer.user is None:
|
||||
cid = cid_user_answer.cid
|
||||
# cid_passcodes[cid] = cid_user_answer.passcode
|
||||
cids.add(cid)
|
||||
cids.add("c/"+cid)
|
||||
else:
|
||||
cid = f"u/{cid_user_answer.user.username}"
|
||||
cid = f"u/{cid_user_answer.user.pk}"
|
||||
cids_user_id_map[cid] = cid_user_answer.user.username
|
||||
cids.add(cid)
|
||||
|
||||
s = cid_user_answer
|
||||
@@ -1439,9 +1532,15 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
unmarked.add(index)
|
||||
user_answers[cid].append(ans)
|
||||
user_answers_marks[cid].append(answer_score)
|
||||
# user_answers_and_marks[cid].append((ans, answer_score))
|
||||
|
||||
if self.app_name in ("rapids", "anatomy", "sbas"):
|
||||
if self.app_name == "rapids":
|
||||
callstate = s.get_answer_callstate()
|
||||
print(cid, callstate)
|
||||
by_question[q][cid] = (ans, answer_score, callstate)
|
||||
user_answers_callstates[cid].append(callstate)
|
||||
print(user_answers_callstates)
|
||||
print("CID:::",cid, user_answers_callstates[cid])
|
||||
elif self.app_name in ("rapids", "anatomy", "sbas"):
|
||||
by_question[q][cid] = (ans, answer_score)
|
||||
else:
|
||||
zipped_ans_scores = zip(ans, answer_score)
|
||||
@@ -1513,33 +1612,53 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
|
||||
exam.stats_graph = fig_html
|
||||
|
||||
exam.user_scores = user_scores
|
||||
user_data = {}
|
||||
for u in cids:
|
||||
#uid = cids_user_id_map[u]
|
||||
user_data[u] = {
|
||||
"score": user_scores[u],
|
||||
"normalised_score": user_scores_normalised[u],
|
||||
}
|
||||
|
||||
if self.app_name == "rapids":
|
||||
counted_callstates = dict(Counter(user_answers_callstates[u]))
|
||||
user_data[u]["callstates"] = str(counted_callstates)
|
||||
user_answers_callstates_counted[u] = counted_callstates
|
||||
|
||||
print(user_data)
|
||||
|
||||
exam.user_scores = user_data
|
||||
|
||||
exam.save()
|
||||
|
||||
template_variables = {
|
||||
"cids": sorted(cids),
|
||||
# "cid_passcodes": cid_passcodes,
|
||||
"exam": exam,
|
||||
"unmarked": unmarked,
|
||||
"questions": questions,
|
||||
"by_question": by_question,
|
||||
# "user_answers": dict(user_answers),
|
||||
# "user_answers_marks": dict(user_answers_marks),
|
||||
"user_scores": user_scores,
|
||||
"user_scores_normalised": user_scores_normalised,
|
||||
# "user_scores_list": user_scores_list,
|
||||
# "user_names": user_names,
|
||||
# "user_answers_and_marks": user_answers_and_marks,
|
||||
"max_score": max_score,
|
||||
"mean": mean,
|
||||
"median": median,
|
||||
"mode": mode,
|
||||
"plot": fig_html,
|
||||
}
|
||||
|
||||
if self.app_name == "rapids":
|
||||
template_variables["user_answers_callstates"] = user_answers_callstates_counted
|
||||
|
||||
return render(
|
||||
request,
|
||||
f"{self.app_name}/exam_scores_new.html",
|
||||
{
|
||||
"cids": sorted(cids),
|
||||
# "cid_passcodes": cid_passcodes,
|
||||
"exam": exam,
|
||||
"unmarked": unmarked,
|
||||
"questions": questions,
|
||||
"by_question": by_question,
|
||||
# "user_answers": dict(user_answers),
|
||||
# "user_answers_marks": dict(user_answers_marks),
|
||||
"user_scores": user_scores,
|
||||
"user_scores_normalised": user_scores_normalised,
|
||||
# "user_scores_list": user_scores_list,
|
||||
# "user_names": user_names,
|
||||
# "user_answers_and_marks": user_answers_and_marks,
|
||||
"max_score": max_score,
|
||||
"mean": mean,
|
||||
"median": median,
|
||||
"mode": mode,
|
||||
"plot": fig_html,
|
||||
},
|
||||
template_variables,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user