Files
penracourses/generic/views.py
T
Ross 9f54a9c108 .
2022-03-23 22:06:33 +00:00

1651 lines
57 KiB
Python

from collections import defaultdict
import statistics
from dal import autocomplete
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.forms.models import model_to_dict
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required, user_passes_test
from django.urls.base import reverse
from django.utils import timezone
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 HttpResponseRedirect, HttpResponse
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.utils.decorators import method_decorator
from django.core.cache import cache
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 CidUserExamTable, CidUserTable
from .forms import (
CidUserForm,
ExaminationForm,
CidUserGroupForm,
)
from .models import CidUser, CidUserGroup, Examination, QuestionNote
from rapids.models import Rapid as RapidQuestion
from rapids.models import Exam as RapidExam
from longs.models import Long as LongQuestion
from longs.models import Exam as LongExam
from anatomy.models import AnatomyQuestion as AnatomyQuestion
from anatomy.models import Exam as AnatomyExam
from sbas.models import Question as SbasQuestion
from sbas.models import Exam as SbasExam
from physics.models import Question as PhysicsQuestion
from physics.models import Exam as PhysicsExam
from django.db.models import Case, When
from django.conf import settings
from datetime import datetime
import os
import string
import random
import plotly.express as px
# from rad.views import get_question_and_content_type
from django.db.models import Prefetch
class CidManagerRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.groups.filter(name="cid_user_manager").exists()
# def get_object(self, *args, **kwargs):
# obj = super().get_object(*args, **kwargs)
# if self.request.user.groups.filter(name="cid_user_manager").exists():
# return obj
# raise PermissionDenied() # or Http404
def normaliseRapidsScore(score):
if score == 49:
return 4.5
elif score in [50, 51]:
return 5
elif score in [52, 53]:
return 5.5
elif score in [54]:
return 6
elif score in [55, 56]:
return 6.5
elif score in [57, 58]:
return 7
elif score in [59]:
return 7.5
elif score in [60]:
return 8
return 4
def get_question_and_content_type(question_type):
if question_type == "rapid":
question = RapidQuestion
content_type = ContentType.objects.get(model="rapid")
elif question_type == "anatomy":
question = AnatomyQuestion
content_type = ContentType.objects.get(model="anatomyquestion")
elif question_type == "long":
question = LongQuestion
content_type = ContentType.objects.get(model="long")
elif question_type == "sbas":
question = SbasQuestion
content_type = ContentType.objects.get(app_label="sbas", model="question")
elif question_type == "physics":
question = PhysicsQuestion
content_type = ContentType.objects.get(app_label="physics", model="question")
else:
raise PermissionError()
return question, content_type
# Create your views here.
@login_required
def create_examination(request):
form = ExaminationForm(request.POST or None)
if form.is_valid():
instance = form.save()
return HttpResponse(
'<script>opener.closePopup(window, "%s", "%s", "#id_examination");</script>'
% (instance.pk, instance)
)
return render(
request, "rapids/create_simple.html", {"form": form, "name": "Examination"}
)
@csrf_exempt
def get_examination_id(request):
if request.is_ajax():
examination_name = request.GET["examination_name"]
examination_id = Examination.objects.get(name=examination_name).id
data = {
"examination_id": examination_id,
}
return HttpResponse(json.dumps(data), content_type="application/json")
return HttpResponse("/")
# Generic view to dispatch to indivuals app views
@login_required
def generic_exam_json_edit(request):
if request.is_ajax() and request.method == "POST":
question_type = request.POST.get("type")
if question_type == "rapid":
Exam = RapidExam
Question = RapidQuestion
elif question_type == "long":
Exam = LongExam
Question = LongQuestion
elif question_type == "anatomy":
Exam = AnatomyExam
Question = AnatomyQuestion
elif question_type == "physics":
Exam = PhysicsExam
Question = PhysicsQuestion
elif question_type == "sbas":
Exam = SbasExam
Question = SbasQuestion
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
exam = get_object_or_404(Exam, pk=request.POST.get("exam_id"))
# if "exam_toggle_active" in request.POST:
# active = json.loads(request.POST.get("active"))
# data = {"status": "success"}
# return JsonResponse(data, status=200)
if "add_exam_questions" in request.POST:
question_ids = json.loads(request.POST.get("add_exam_questions"))
# question_objects = Question.objects.filter(pk__in=question_ids)
add_count = 0
for i in question_ids:
# We get an integrity error if we try to add an object that already exists
if not exam.exam_questions.filter(pk=i).exists():
add_count += 1
exam.exam_questions.add(i)
if add_count > 0:
exam.save()
data = {"status": "success", "questions added": add_count}
return JsonResponse(data, status=200)
if "set_exam_order" in request.POST:
new_order = json.loads(request.POST.get("set_exam_order"))
preserved = Case(
*[When(pk=pk, then=pos) for pos, pk in enumerate(new_order)]
)
objects = Question.objects.filter(pk__in=new_order).order_by(preserved)
if len(objects) != exam.exam_questions.count():
data = {"status": "error, count does not match"}
return JsonResponse(data, status=400)
exam.exam_questions.set(objects)
exam.save()
data = {"status": "success"}
return JsonResponse(data, status=200)
# Only checkers can do the following
if (
question_type == "rapid"
and not request.user.groups.filter(name="rapid_checker").exists()
):
data = {"status": "error, permission denied"}
return JsonResponse(data, status=400)
if (
question_type == "anatomy"
and not request.user.groups.filter(name="anatomy_checker").exists()
):
data = {"status": "error, permission denied"}
return JsonResponse(data, status=400)
if (
question_type == "long"
and not request.user.groups.filter(name="long_checker").exists()
):
data = {"status": "error, permission denied"}
return JsonResponse(data, status=400)
if "set_open_access" in request.POST:
if request.POST.get("set_open_access") == "true":
state = True
else:
state = False
questions_changed = []
for q in exam.exam_questions.all():
q.open_access = state
q.save()
questions_changed.append(q.pk)
data = {"status": "success", "questions": questions_changed, "state": state}
return JsonResponse(data, status=200)
data = {"status": "error, unknown request"}
return JsonResponse(data, status=400)
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
class ExamViews(View, LoginRequiredMixin):
def __init__(
self,
exam,
question,
answer,
cid_user_answer,
app,
question_type,
loadJsonAnswer,
):
self.Exam = exam
self.Question = question
self.Answer = answer
self.CidUserAnswer = cid_user_answer
self.app_name = app
self.question_type = question_type
self.loadJsonAnswer = loadJsonAnswer
# THis may be better than check_user_access below
# group_map = {"rapids" : "rapid_checker", "anatomy" : "anatomy_checker", "longs":"long_checker"}
# exam_group = group_map[self.app_name]
def check_user_access(self, user, exam_id=None):
"""Check if a user should be able to access a view
Args:
user ([user]): user object
Returns:
[boolean]: True if the user has access
"""
print("****", user, exam_id)
if exam_id is not None:
exam = get_object_or_404(self.Exam, pk=exam_id)
if (exam.open_access and exam.active) or user in exam.get_author_objects():
return True
if (
self.app_name == "rapids"
and not user.groups.filter(name="rapid_checker").exists()
):
return False
if (
self.app_name == "anatomy"
and not user.groups.filter(
name__in=("anatomy_checker", "anatomy_marker")
).exists()
):
return False
if (
self.app_name == "longs"
and not user.groups.filter(
name__in=("long_checker", "long_marker")
).exists()
):
return False
if (
self.app_name == "physics"
and not user.groups.filter(name="physics_checker").exists()
):
return False
if (
self.app_name == "sbas"
and not user.groups.filter(name="sba_checker").exists()
):
return False
return True
def check_user_edit_access(self, user, exam_id=None):
"""Check if a user should be able to access a view
Args:
user ([user]): user object
Returns:
[boolean]: True if the user has access
"""
# If a user is an exam author they should have acccess
if exam_id is not None:
exam = get_object_or_404(self.Exam, pk=exam_id)
if exam.open_access or user in exam.get_author_objects():
return True
if (
self.app_name == "rapids"
and not user.groups.filter(name="rapid_checker").exists()
):
return False
if (
self.app_name == "anatomy"
and not user.groups.filter(name="anatomy_checker").exists()
):
return False
if (
self.app_name == "longs"
and not user.groups.filter(name__in=("long_checker",)).exists()
):
return False
if (
self.app_name == "physics"
and not user.groups.filter(name="physics_checker").exists()
):
return False
if (
self.app_name == "sbas"
and not user.groups.filter(name="sba_checker").exists()
):
return False
return True
@method_decorator(login_required)
def exam_toggle_results_published(self, request, pk):
if request.is_ajax() and request.method == "POST":
if not self.check_user_edit_access(request.user):
data = {"status": "error, insufficient permission"}
return JsonResponse(data, status=400)
exam = get_object_or_404(self.Exam, pk=pk)
exam.publish_results = (
True if request.POST.get("publish_results") == "true" else False
)
exam.save()
data = {
"status": "success",
"publish_results": exam.publish_results,
"name": exam.name,
"id": exam.id,
}
return JsonResponse(data, status=200)
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
@method_decorator(login_required)
def exam_toggle_active(self, request, pk):
if request.is_ajax() and request.method == "POST":
if not self.check_user_edit_access(request.user):
data = {"status": "error, insufficient permission"}
return JsonResponse(data, status=400)
exam = get_object_or_404(self.Exam, pk=pk)
exam.active = True if request.POST.get("active") == "true" else False
exam.save()
data = {
"status": "success",
"active": exam.active,
"name": exam.name,
"id": exam.id,
}
return JsonResponse(data, status=200)
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
@method_decorator(login_required)
def exam_json_recreate(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
exam.recreate_json = True
exam.save()
exam.get_exam_json()
return redirect("{}:exam_overview".format(self.app_name), pk=pk)
@method_decorator(login_required)
def question_json_recreate(self, request, pk):
question = get_object_or_404(self.Question, pk=pk)
question.recreate_json = True
question.save()
return redirect("{}:question_detail".format(self.app_name), pk=pk)
@method_decorator(login_required)
def exam_list_all(self, request):
return self.exam_list(request, all=True)
@method_decorator(login_required)
def exam_list(self, request, all=False):
if all:
exams = self.Exam.objects.all().order_by("name")
else:
exams = self.Exam.objects.filter(exam_mode=True, archive=False).order_by(
"name"
)
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,
"marking": marking,
},
)
@method_decorator(login_required)
def exam_overview(self, request, pk):
# 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=pk)
if request.user not in exam.author.all():
if not self.check_user_access(request.user, pk):
raise PermissionDenied
can_edit = (
self.check_user_edit_access(request.user, pk) | request.user.is_superuser
)
notes = []
if can_edit:
notes = self.exam_notes(request, pk)
if self.app_name == "anatomy":
questions = (
exam.exam_questions.select_related(
"modality",
"structure",
"body_part",
"region",
"examination",
"question_type",
)
.all()
.prefetch_related(
Prefetch(
"answers",
queryset=self.Answer.objects.filter(
proposed=False, status=self.Answer.MarkOptions.CORRECT
),
to_attr="prefetched_primary_answer"
# queryset=self.Answer.objects.filter(),
),
)
)
elif self.app_name == "rapids":
questions = (
exam.exam_questions.select_related()
.all()
.prefetch_related("images", "abnormality", "region", "examination")
)
elif self.app_name == "physics":
questions = (
exam.exam_questions.select_related("category").all()
# .prefetch_related("images", "abnormality", "region", "examination")
)
else:
questions = exam.exam_questions.select_related().all()
question_number = len(questions)
return render(
request,
"{}/exam_overview.html".format(self.app_name),
{
"exam": exam,
"questions": questions,
"question_number": question_number,
"can_edit": can_edit,
"notes": notes,
"app_name": self.app_name,
},
)
@method_decorator(login_required)
def exam_scores_refresh(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
if not exam.exam_mode:
raise Http404("Packet not in exam mode")
questions = exam.exam_questions.all()
cids = self.CidUserAnswer.objects.filter(question__in=questions, exam__id=pk)
# Force a score update
for c in cids:
c.get_answer_score(cached=False)
return render(
request,
f"{self.app_name}/base.html",
{
"simple_content": format_html("""Answer scores updated <br/>
<a href='{}'>Return</a>""", reverse(f"{self.app_name}:exam_scores_cid", kwargs={"pk": exam.pk}))
},
)
def exam_cids(self, request, exam_id):
exam = get_object_or_404(self.Exam, pk=exam_id)
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
cid_users = exam.valid_users.all()
cid_user_count = cid_users.count()
return render(
request,
"exam_cids.html",
{
"exam": exam,
"cid_users": cid_users,
"cid_user_count": cid_user_count,
"app_name": self.app_name,
},
)
# @method_decorator(login_required)
def exam_notes(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
if request.user not in exam.author.all():
if not self.check_user_access(request.user, pk):
raise PermissionDenied
q, content_type = get_question_and_content_type(self.question_type)
question_pks = exam.exam_questions.values_list("pk", flat=True)
# Get active notes for type
notes = QuestionNote.objects.filter(
content_type=content_type, object_id__in=question_pks, complete=False
)
return notes
@method_decorator(login_required)
def exam_json_edit(self, request, pk):
if request.is_ajax() and request.method == "POST":
if not self.check_user_edit_access(request.user, exam_id=pk):
data = {"status": "invalid permisions"}
return JsonResponse(data, status=400)
exam = get_object_or_404(self.Exam, pk=pk)
if "add_exam_questions" in request.POST:
question_ids = json.loads(request.POST.get("add_exam_questions"))
question_objects = self.Question.objects.filter(pk__in=question_ids)
exam.exam_questions.add(question_objects)
exam.save()
data = {"status": "success"}
return JsonResponse(data, status=200)
if "set_exam_order" in request.POST:
new_order = json.loads(request.POST.get("set_exam_order"))
preserved = Case(
*[When(pk=pk, then=pos) for pos, pk in enumerate(new_order)]
)
objects = self.Question.objects.filter(pk__in=new_order).order_by(
preserved
)
# We now allow deleting exam questions
# if len(objects) != exam.exam_questions.count():
# data = {"status": "error, count does not match"}
# return JsonResponse(data, status=400)
exam.exam_questions.set(objects)
exam.save()
data = {"status": "success"}
return JsonResponse(data, status=200)
if not self.check_user_access(request.user, pk):
raise PermissionDenied
if "set_open_access" in request.POST:
if request.POST.get("set_open_access") == "true":
state = True
else:
state = False
questions_changed = []
for q in exam.exam_questions.all():
q.open_access = state
q.save()
questions_changed.append(q.pk)
data = {
"status": "success",
"questions": questions_changed,
"state": state,
}
return JsonResponse(data, status=200)
data = {"status": "error, unknown request"}
return JsonResponse(data, status=400)
else:
data = {"status": "error"}
return JsonResponse(data, status=400)
@method_decorator(login_required)
def mark_overview(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
if not exam.exam_mode:
raise Http404("Packet not in exam mode")
if request.user not in exam.author.all():
if not self.check_user_access(request.user, pk):
raise PermissionDenied
if self.app_name == "anatomy":
questions = (
exam.exam_questions.select_related(
"modality",
"structure",
"body_part",
"region",
"examination",
"question_type",
)
.all()
.prefetch_related(
# "cid_user_answers",
Prefetch(
"answers",
queryset=self.Answer.objects.filter(
proposed=False, status=self.Answer.MarkOptions.CORRECT
),
to_attr="prefetched_primary_answer"
# queryset=self.Answer.objects.filter(),
),
# Prefetch(
# "cid_user_answers",
# queryset=self.CidUserAnswer.objects.filter(score=self.Answer.MarkOptions.UNMARKED, exam__id=pk),
# to_attr="prefetched_unmarked_cid_answers"
# #queryset=self.Answer.objects.filter(),
# ),
)
)
else:
questions = exam.exam_questions.all()
# Handle exams that require double marking
try:
if exam.double_mark:
question_unmarked_map = []
for q in questions:
question_unmarked_map.append(
(
q,
int(q.get_unmarked_user_answer_count(exam_pk=exam.pk)),
int(
q.get_unmarked_user_answer_count(
exam_pk=exam.pk, marker=request.user
)
),
)
)
return render(
request,
"{}/mark_overview.html".format(self.app_name),
{"exam": exam, "question_unmarked_map": question_unmarked_map},
)
except AttributeError:
pass
question_unmarked_map = []
for q in questions:
count = int(q.get_unmarked_user_answer_count(exam_pk=exam.pk))
question_unmarked_map.append(
(q, count, count)
)
return render(
request,
"{}/mark_overview.html".format(self.app_name),
{"exam": exam, "question_unmarked_map": question_unmarked_map},
)
@method_decorator(login_required)
def index(self, request):
exams = self.Exam.objects.filter(author__id=request.user.id)
if (
self.app_name == "rapids"
and request.user.groups.filter(name="rapid_checker").exists()
):
exams = self.Exam.objects.all()
if (
self.app_name == "anatomy"
and request.user.groups.filter(name="anatomy_checker").exists()
):
exams = self.Exam.objects.all()
if (
self.app_name == "longs"
and request.user.groups.filter(name="long_checker").exists()
):
exams = self.Exam.objects.all()
return render(request, "{}/index.html".format(self.app_name), {"exams": exams})
@method_decorator(login_required)
def exam_question_detail(self, request, pk, sk):
exam = get_object_or_404(self.Exam, pk=pk)
if request.user not in exam.author.all():
if not self.check_user_access(request.user, pk):
raise PermissionDenied
question = exam.exam_questions.all()[sk]
exam_length = len(exam.exam_questions.all())
pos = exam.get_question_index(question) + 1
previous = -1
if sk > 0:
previous = sk - 1
next = sk + 1
if sk == exam_length - 1:
next = False
return render(
request,
"{}/question_detail.html".format(self.app_name),
{
"question": question,
"exam": exam,
"next": next,
"previous": previous,
"exam_length": exam_length,
"pos": pos,
},
)
def active_exams(self, request, json=True, based=True, cid=None, passcode=None):
exams = self.Exam.objects.filter(archive=False)
active_exams = {"exams": []}
if cid is not None:
cid_user = CidUser.objects.filter(cid=cid).first()
if not cid_user or cid_user.passcode != passcode:
if json == False:
return active_exams["exams"]
return JsonResponse({"status": "invalid"}, status=401)
for exam in exams:
if exam.active or self.check_user_access(request.user, exam.pk):
print(exam.name, cid, passcode)
if exam.exam_mode and not exam.check_cid_user(cid, passcode):
print(exam.name, "fail")
continue
if exam.json_creation_time:
creation_time = exam.json_creation_time.isoformat()
else:
creation_time = "None"
url = request.build_absolute_uri(exam.get_json_url())
# hacky
if not based:
url = url + "/unbased"
obj = {
"name": exam.get_exam_name(),
"url": url,
"type": self.question_type,
"eid": "{}/{}".format(self.question_type, exam.pk),
"json_creation_time": creation_time,
"exam_json_id": exam.exam_json_id,
"exam_active": exam.active,
"exam_mode": exam.exam_mode,
}
if self.question_type == "long":
h = {}
for question in exam.exam_questions.all():
# Generate json if needed
if question.json_creation_time is None:
question.get_question_json()
h[question.pk] = question.question_json_id
obj["multi_question_json"] = h
active_exams["exams"].append(obj)
if json == False:
return active_exams["exams"]
return JsonResponse(active_exams)
@method_decorator(csrf_exempt)
def postExamAnswers(self, request):
if request.is_ajax and request.method == "POST":
n = 0
for answer in json.loads(request.POST.get("answers")):
ret_status, ret = self.loadJsonAnswer(answer)
# try:
# ret_status, ret = self.loadJsonAnswer(answer)
# except:
# # We catch the error here so users get an error (rather than 500)
# return JsonResponse({"success": False, "error": "Invalid data"})
# if ret is not True:
# return ret
if ret_status:
n = n + 1
else:
return ret
# print(UserAnswer.objects.filter(exam__id=q["eid"]))
# print(request.urlencode())
exam_type, exam_id = request.POST.get("eid").split("/")
c = CidUser.objects.filter(cid=request.POST.get("cid")).first()
exam = self.Exam.objects.get(pk=exam_id)
t = timezone.make_aware(
datetime.fromtimestamp(float(request.POST.get("start_time")))
)
cid_user_exam = exam.get_or_create_cid_user_exam(cid_user=c, start_time=t)
cid_user_exam.end_time = timezone.now()
cid_user_exam.save()
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)
if not exam.active and not self.check_user_access(request.user, pk):
raise Http404("No available exam")
exam_json = exam.get_exam_json(based=False)
# 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):
raise Http404("No available exam")
# exam_json_cache = cache.get("{}_exam_json_{}".format(self.app_name, pk))
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)
if os.path.isfile(path) and not exam.recreate_json:
# exam_json_cache["cached"] = True
# Make sure we pass on an argument if we are forcing a reload
if request.GET.get("_"):
query_string = urllib.parse.urlencode({"_": request.GET.get("_")})
url = "{}?{}".format(url, query_string)
return redirect(url)
return redirect(url)
return JsonResponse(exam_json_cache)
time = datetime.now()
exam.exam_json_id += 1
with open(path, "w+") as f:
exam_json = exam.get_exam_json()
exam_json["generated"] = time.isoformat()
exam_json["exam_json_id"] = exam.exam_json_id
f.write(json.dumps(exam_json))
exam.recreate_json = False
exam.json_creation_time = time
exam.save()
# We also try to clear the cache of any associated questions (longs)
# see exam_question_json
# n = exam.exam_questions.count()
# question_keys = ["{}_exam_json_{}_{}".format(self.app_name, pk, i) for i in range(1, n)]
# cache.delete_many(question_keys)
# cache.set("{}_exam_json_{}".format(self.app_name, pk), exam_json, 3600)
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)
if not exam.active and not self.check_user_access(request.user, pk):
raise Http404("No available exam")
if request.GET.get("_"):
base_url = redirect("{}:question_json".format(self.app_name), pk=sk)
query_string = urllib.parse.urlencode({"_": request.GET.get("_")})
url = "{}?{}".format(base_url, query_string)
return redirect(url)
return redirect("{}:question_json".format(self.app_name), pk=sk)
def exam_question_json_unbased(self, request, pk, sk):
question = get_object_or_404(self.Question, pk=sk)
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")
return redirect("{}:question_json_unbased".format(self.app_name), pk=sk)
redirect()
question_json_cache = cache.get("{}_question_json_{}".format(self.app_name, sk))
if question_json_cache is not None and not question.recreate_json:
question_json_cache["cached"] = True
return JsonResponse(question_json_cache)
question_json = exam.get_exam_question_json(sk)
cache.set("{}_question_json_{}".format(self.app_name, sk), question_json, 3600)
return JsonResponse(question_json)
def exam_scores_cid(self, request, pk):
exam = get_object_or_404(self.Exam, pk=pk)
if not exam.exam_mode:
raise Http404("Packet not in exam mode")
user_answers_and_marks = defaultdict(list)
user_answers_marks = defaultdict(list)
user_answers = defaultdict(list)
user_names = {}
# cid_passcodes = {}
by_question = defaultdict(dict)
unmarked = set()
questions = exam.exam_questions.all()
# We could prefect the CidUserAnswers.answers here (if we didn't cache them)
cid_user_answers = self.CidUserAnswer.objects.select_related("question").filter(
question__in=questions, exam__id=pk
)
cids = set()
# Loop through all candidates
for cid_user_answer in cid_user_answers:
# Convoluted (probably...)
cid = cid_user_answer.cid
# cid_passcodes[cid] = cid_user_answer.passcode
cids.add(cid)
s = cid_user_answer
user_names[cid] = cid
q = cid_user_answer.question
# if not s:
# # skip if no answer
# user_answers_marks[cid].append(0)
# user_answers[cid].append("")
# by_question[q].append(("", 0))
# continue
ans = s.get_answer_string()
answer_score = s.get_answer_score()
if answer_score == "unmarked":
index = exam.get_question_index(q)
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"):
by_question[q][cid] = (ans, answer_score)
else:
zipped_ans_scores = zip(ans, answer_score)
by_question[q][cid] = zipped_ans_scores
user_scores = {}
user_scores_normalised = {}
for user in user_answers_marks:
if self.app_name in ("rapids", "anatomy", "sbas"):
user_scores[user] = sum(
[i for i in user_answers_marks[user] if i != "unmarked"]
)
else:
user_scores[user] = sum(
[sum(i) for i in user_answers_marks[user] if i != "unmarked"]
)
if self.app_name == "rapids":
user_scores_normalised[user] = normaliseRapidsScore(
sum([i for i in user_answers_marks[user] if i != "unmarked"])
)
else:
user_scores_normalised[user] = user_scores[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
elif self.app_name == "physics":
max_score = len(questions) * 5
else:
max_score = len(questions)
if len(user_scores_list) < 1:
mean = 0
median = 0
mode = 0
fig_html = ""
else:
mean = statistics.mean(user_scores_list)
median = statistics.median(user_scores_list)
try:
mode = statistics.mode(user_scores_list)
except statistics.StatisticsError:
mode = "No unique mode"
df = user_scores_list
fig = px.histogram(
df,
x=0,
title="{}: distribution of scores".format(exam),
labels={"0": "Score"},
height=400,
width=600,
)
fig_html = fig.to_html()
exam.stats_mean = mean
exam.stats_median = median
exam.stats_mode = mode
exam.stats_candidates = len(user_scores_list)
exam.stats_max_possible = max_score
exam.stats_min = min(user_scores_list)
exam.stats_max = max(user_scores_list)
exam.stats_graph = fig_html
exam.user_scores = user_scores
exam.save()
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,
},
)
class GenericViewBase:
def __init__(self, app_name, QuestionObject, CidUserAnswerObject, ExamObject):
self.question_object = QuestionObject
self.cid_user_answer_object = CidUserAnswerObject
self.exam_object = ExamObject
self.app_name = app_name
g = {
"rapids": "rapid_checker",
"anatomy": "anatomy_checker",
"longs": "longs_checker",
"sbas": "sbas_checker",
"physics": "physics_checker",
}
self.checker_group = g[app_name]
def question_review(self, request, pk):
"""
Return a json representation of the question when the exam is active
"""
if request.is_ajax():
exam = get_object_or_404(self.exam_object, pk=pk)
print(request.POST["question_number"])
print(type(request.POST["question_number"]))
if not exam.publish_results:
raise Http404
question = exam.exam_questions.all()[int(request.POST["question_number"])]
question_json = question.get_question_json(based=False)
return JsonResponse(question_json)
return JsonResponse({"status": "error"})
@method_decorator(user_passes_test(lambda u: u.is_superuser))
def user_answer_delete_multiple(self, request):
if request.is_ajax():
print(request.POST["answer_ids"])
answer_ids = json.loads(request.POST["answer_ids"])
# We could probably delete them all at once....
for id in answer_ids:
self.cid_user_answer_object.objects.get(pk=id).delete()
return JsonResponse({"status": "success"})
return JsonResponse({"status": "error"})
@method_decorator(login_required)
def question_detail(self, request, pk):
question = get_object_or_404(self.question_object, pk=pk)
if not question.open_access:
if (
not request.user.groups.filter(name=self.checker_group).exists()
and request.user not in question.author.all()
):
raise PermissionDenied()
# if request.user not in rapid.author.all():
# raise PermissionDenied
view_feedback = False
if (
request.user.groups.filter(name=self.checker_group).exists()
or request.user.groups.filter(name="feedback_checker").exists()
or request.user in question.author.all()
):
view_feedback = True
# logging.debug(rapid.subspecialty.first().name.all())
return render(
request,
f"{self.app_name}/question_detail.html",
{"question": question, "view_feedback": view_feedback},
)
@method_decorator(login_required)
def question_user_answers(self, request, pk):
question = get_object_or_404(self.question_object, pk=pk)
answers = self.cid_user_answer_object.objects.filter(question__id=question.pk)
return render(
request,
f"{self.app_name}/question_user_answers.html",
{"question": question, "answers": answers},
)
@method_decorator(login_required)
def author_detail(self, request, pk):
# logging.debug(Author.objects.all())
# author = get_object_or_404(Author, pk=pk)
author = User.objects.get(pk=pk)
questions = self.question_object.objects.filter(author=pk)
return render(
request,
f"{self.app_name}/category_detail.html",
{"category": author, "questions": questions},
)
@method_decorator(login_required)
def author_list(self, request):
authors = User.objects.all()
return render(
request, f"{self.app_name}/author_list.html", {"authors": authors}
)
class ExamCloneMixin:
def get_initial(self):
old_object = get_object_or_404(self.model, pk=self.kwargs["exam_id"])
initial_data = model_to_dict(old_object, exclude=["id"])
questions = old_object.exam_questions.all().values_list("id", flat=True)
self.exam_questions = list(questions)
return initial_data
def form_valid(self, form):
object = form.save()
object.exam_questions.set(self.exam_questions)
object.save()
return HttpResponseRedirect(object.get_absolute_url())
class ExaminationAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
# Don't forget to filter out results depending on the visitor !
if not self.request.user.is_authenticated:
return Examination.objects.none()
qs = Examination.objects.all()
if self.q:
# This raises a fielderror which breaks creating a new item if not caught
try:
qs = qs.filter(examination__icontains=self.q)
except FieldError:
return Examination.objects.none()
return qs
class CidUserExamView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
model = CidUser
table_class = CidUserExamTable
template_name = "generic/cid_view.html"
filterset_class = CidUserFilter
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# user = self.request.user
filters = {"archive": False, "exam_mode": True}
physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)]
rapid_exams = [(i.name, i.pk) for i in RapidExam.objects.filter(**filters)]
sba_exams = [(i.name, i.pk) for i in SbasExam.objects.filter(**filters)]
longs_exams = [(i.name, i.pk) for i in LongExam.objects.filter(**filters)]
anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
cid_user_groups = [
(i.name, i.pk) for i in CidUserGroup.objects.filter(archive=False)
]
context["physics_exams"] = physics_exams
context["rapid_exams"] = rapid_exams
context["sba_exams"] = sba_exams
context["longs_exams"] = longs_exams
context["anatomy_exams"] = anatomy_exams
context["cid_user_groups"] = cid_user_groups
context["exams"] = True
return context
class CidUserView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
model = CidUser
table_class = CidUserTable
template_name = "generic/cid_view.html"
filterset_class = CidUserFilter
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# user = self.request.user
filters = {"archive": False, "exam_mode": True}
physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)]
rapid_exams = [(i.name, i.pk) for i in RapidExam.objects.filter(**filters)]
sba_exams = [(i.name, i.pk) for i in SbasExam.objects.filter(**filters)]
longs_exams = [(i.name, i.pk) for i in LongExam.objects.filter(**filters)]
anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
cid_user_groups = [
(i.name, i.pk) for i in CidUserGroup.objects.filter(archive=False)
]
context["physics_exams"] = physics_exams
context["rapid_exams"] = rapid_exams
context["sba_exams"] = sba_exams
context["longs_exams"] = longs_exams
context["anatomy_exams"] = anatomy_exams
context["cid_user_groups"] = cid_user_groups
cid_user_groups = [
(i.name, i.pk) for i in CidUserGroup.objects.filter(archive=False)
]
context["cid_user_groups"] = cid_user_groups
return context
@login_required
@user_is_cid_user_manager
def group_view(request):
groups = CidUserGroup.objects.filter(archive=False)
return render(
request,
"generic/group_view.html",
{"groups": groups},
)
@user_is_cid_user_manager
def group_view(request):
groups = CidUserGroup.objects.filter(archive=False)
return render(
request,
"generic/group_view.html",
{"groups": groups, "view_all": False},
)
@user_is_cid_user_manager
def group_view_all(request):
groups = CidUserGroup.objects.filter()
return render(
request,
"generic/group_view.html",
{"groups": groups, "view_all": True},
)
@login_required
@user_is_cid_user_manager
def group_email_resend(request, pk):
return group_email(request, pk, resend=True)
@login_required
@user_is_cid_user_manager
def group_email_results_resend(request, pk):
return group_email_results(request, pk, resend=True)
@login_required
@user_is_cid_user_manager
def group_email(request, pk, resend=False):
group = get_object_or_404(CidUserGroup, pk=pk)
if resend:
users = group.ciduser_set.filter(active=True)
else:
users = group.ciduser_set.filter(active=True, login_email_sent=False)
return render(
request,
"generic/group_emailed.html",
#{"sent": sent, "not_sent": not_sent, "users_count": users.count()},
{"users": users, "users_count": users.count()},
)
#sent = []
#not_sent = []
#for u in users:
# success, msg = u.email_details(resend=resend)
# if success:
# sent.append(u)
# else:
# not_sent.append((u, msg))
#return render(
# request,
# "generic/group_emailed.html",
# {"sent": sent, "not_sent": not_sent, "users_count": users.count()},
#)
@user_is_cid_user_manager
def group_email_results(request, pk, resend=False):
group = get_object_or_404(CidUserGroup, pk=pk)
if resend:
users = group.ciduser_set.filter(active=True, internal_candidate=True)
else:
users = group.ciduser_set.filter(active=True, internal_candidate=True, results_email_sent=False)
#sent = []
#not_sent = []
#for u in users:
# success, msg = u.email_results(resend=resend)
# if success:
# sent.append(u)
# else:
# not_sent.append((u, msg))
return render(
request,
"generic/group_emailed.html",
#{"sent": sent, "not_sent": not_sent, "users_count": users.count()},
{"users": users, "users_count": users.count()},
)
@user_is_cid_user_manager
def candidate_email_results(request, cid, resend=False):
user = CidUser.objects.get(cid=cid)
email_sent, comment = user.email_results(additional_emails=["priya.suresh@nhs.net"], resend=True)
return JsonResponse({"sent": email_sent, "comment": comment})
@user_is_cid_user_manager
def candidate_email_results_resend(request, cid, resend=True):
return candidate_email_results(request, cid, resend=True)
@user_is_cid_user_manager
def manage_cid_users(request):
if request.is_ajax() and request.method == "POST":
physics_exams = json.loads(request.POST.get("physics_exams"))
rapid_exams = json.loads(request.POST.get("rapid_exams"))
sba_exams = json.loads(request.POST.get("sba_exams"))
longs_exams = json.loads(request.POST.get("longs_exams"))
anatomy_exams = json.loads(request.POST.get("anatomy_exams"))
cid_groups = json.loads(request.POST.get("cid_groups"))
selected_cids = json.loads(request.POST.get("selected_cids"))
emails = json.loads(request.POST.get("emails"))
number_to_create = int(request.POST.get("number_to_create"))
add_group = request.POST.get("add_group")
delete = request.POST.get("delete")
activate = request.POST.get("activate")
deactivate = request.POST.get("deactivate")
add_exams = request.POST.get("add_exams")
clear_exams = request.POST.get("clear_exams")
toggle_internal = request.POST.get("toggle_internal")
if add_group == "true":
cids = CidUser.objects.filter(pk__in=selected_cids)
for c in cids:
c.group_id = cid_groups[0]
c.save()
return JsonResponse({"status": "success"})
if delete == "true":
cids = CidUser.objects.filter(pk__in=selected_cids).delete()
return JsonResponse({"status": "success"})
if activate == "true":
cids = CidUser.objects.filter(pk__in=selected_cids)
for c in cids:
c.active = True
c.save()
return JsonResponse({"status": "success"})
if toggle_internal == "true":
cids = CidUser.objects.filter(pk__in=selected_cids)
for c in cids:
c.internal_candidate = not c.internal_candidate
c.save()
return JsonResponse({"status": "success"})
if deactivate == "true":
cids = CidUser.objects.filter(pk__in=selected_cids)
for c in cids:
c.active = False
c.save()
return JsonResponse({"status": "success"})
if clear_exams == "true":
cids = CidUser.objects.filter(pk__in=selected_cids)
for c in cids:
c.physics_exams.clear()
c.rapid_exams.clear()
c.sba_exams.clear()
c.longs_exams.clear()
c.anatomy_exams.clear()
return JsonResponse({"status": "success"})
if add_exams == "true":
cids = CidUser.objects.filter(pk__in=selected_cids)
for c in cids:
if physics_exams:
c.physics_exams.set(physics_exams)
c.save()
if rapid_exams:
c.rapid_exams.set(rapid_exams)
c.save()
if sba_exams:
c.sba_exams.set(sba_exams)
c.save()
if longs_exams:
c.longs_exams.set(longs_exams)
c.save()
if anatomy_exams:
c.anatomy_exams.set(anatomy_exams)
c.save()
return JsonResponse({"status": "success"})
try:
new_cid = CidUser.objects.all().order_by("-cid")[0].cid + 1
except IndexError:
new_cid = 10000
if emails:
email_list = [i.strip() for i in emails.split()]
for e in email_list:
passcode = "".join(random.choices(string.ascii_uppercase, k=4))
c = CidUser(cid=new_cid, passcode=passcode, email=e)
c.save()
if cid_groups:
c.group_id = cid_groups[0]
if physics_exams:
c.physics_exams.set(physics_exams)
c.save()
if rapid_exams:
c.rapid_exams.set(rapid_exams)
c.save()
if sba_exams:
c.sba_exams.set(sba_exams)
c.save()
if longs_exams:
c.longs_exams.set(longs_exams)
c.save()
if anatomy_exams:
c.anatomy_exams.set(anatomy_exams)
c.save()
new_cid = new_cid + 1
for n in range(number_to_create):
passcode = "".join(random.choices(string.ascii_uppercase, k=4))
c = CidUser(cid=new_cid, passcode=passcode)
c.save()
if cid_groups:
c.group_id = cid_groups[0]
if physics_exams:
c.physics_exams.set(physics_exams)
c.save()
if rapid_exams:
c.rapid_exams.set(rapid_exams)
c.save()
if sba_exams:
c.sba_exams.set(sba_exams)
c.save()
if longs_exams:
c.longs_exams.set(longs_exams)
c.save()
if anatomy_exams:
c.anatomy_exams.set(anatomy_exams)
c.save()
new_cid = new_cid + 1
return JsonResponse({"status": "success"})
return JsonResponse({"status": "fail"})
class CidUserCreate(RevisionMixin, CidManagerRequiredMixin, CreateView):
model = CidUser
form_class = CidUserForm
class CidUserUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView):
model = CidUser
form_class = CidUserForm
class CidUserGroupCreate(RevisionMixin, CidManagerRequiredMixin, CreateView):
model = CidUserGroup
form_class = CidUserGroupForm
class CidUserGroupUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView):
model = CidUserGroup
form_class = CidUserGroupForm