lost track of changes...
This commit is contained in:
+150
-35
@@ -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())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user