shorts might now be functional?
This commit is contained in:
@@ -43,7 +43,7 @@
|
||||
<h3>Stats</h3>
|
||||
Candidates: {{cids|length}}<br />
|
||||
Questions: <span id="question-number">{{question_number}}</span><br />
|
||||
Available marks: {{max_score}}<br />
|
||||
<span title="Max score">Available marks: {{max_score}}</span><br />
|
||||
Mean: {{mean}}, Median {{median}}, Mode {{mode}}<br />
|
||||
Top score: {{exam.stats_max}}
|
||||
|
||||
|
||||
+73
-35
@@ -571,39 +571,44 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
|
||||
if (
|
||||
self.app_name == "rapids"
|
||||
and not user.groups.filter(name="rapid_checker").exists()
|
||||
and user.groups.filter(name="rapid_checker").exists()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
if (
|
||||
self.app_name == "shorts"
|
||||
and user.groups.filter(name="shorts").exists()
|
||||
):
|
||||
return True
|
||||
if (
|
||||
self.app_name == "anatomy"
|
||||
and not user.groups.filter(
|
||||
name__in=("anatomy_checker", "anatomy_marker")
|
||||
and user.groups.filter(
|
||||
name__in=("anatomy_checker", "anatomy_marker")
|
||||
).exists()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
if (
|
||||
self.app_name == "longs"
|
||||
and not user.groups.filter(
|
||||
name__in=("long_checker", "long_marker")
|
||||
and user.groups.filter(
|
||||
name__in=("long_checker", "long_marker")
|
||||
).exists()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
if (
|
||||
self.app_name == "physics"
|
||||
and not user.groups.filter(name="physics_checker").exists()
|
||||
and user.groups.filter(name="physics_checker").exists()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
if (
|
||||
self.app_name == "sbas"
|
||||
and not user.groups.filter(name="sba_checker").exists()
|
||||
and user.groups.filter(name="sba_checker").exists()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
if (
|
||||
self.app_name == "atlas"
|
||||
and not user.groups.filter(name="casecollection_checker").exists()
|
||||
and user.groups.filter(name="casecollection_checker").exists()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_user_marker_access(self, user: User, exam_id: int = None):
|
||||
return self.check_user_access(user, exam_id, marker=True)
|
||||
@@ -1367,6 +1372,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
|
||||
app_exam_map = {}
|
||||
app_exam_map["rapids"] = cid_user.rapid_exams
|
||||
app_exam_map["shorts"] = cid_user.shorts_exams
|
||||
app_exam_map["anatomy"] = cid_user.anatomy_exams
|
||||
app_exam_map["longs"] = cid_user.longs_exams
|
||||
app_exam_map["physics"] = cid_user.physics_exams
|
||||
@@ -1849,6 +1855,24 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
ans.answer = ""
|
||||
elif answer["qidn"] == "2" and not ans.normal:
|
||||
ans.answer = posted_answer
|
||||
case "shorts":
|
||||
if not existing_answers:
|
||||
if uid:
|
||||
ans = self.UserAnswer(
|
||||
answer=posted_answer, user=User.objects.get(id=uid)
|
||||
)
|
||||
else:
|
||||
ans = self.UserAnswer(answer=posted_answer, cid=cid)
|
||||
|
||||
ans.question_id = answer["qid"]
|
||||
ans.exam_id = eid
|
||||
ans.score = None
|
||||
else:
|
||||
# Update an existing answer
|
||||
# should never be more than one (famous last words)
|
||||
ans = existing_answers[0]
|
||||
ans.answer = posted_answer
|
||||
ans.score = None
|
||||
case "anatomy":
|
||||
if not existing_answers:
|
||||
if uid:
|
||||
@@ -2275,15 +2299,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
else:
|
||||
total_score = sum(answers_marks)
|
||||
|
||||
match self.app_name:
|
||||
case "rapids" | "anatomy":
|
||||
max_score = len(questions) * 2
|
||||
case "longs":
|
||||
max_score = len(questions) * 8
|
||||
case "physics":
|
||||
max_score = len(questions) * 5
|
||||
case _:
|
||||
max_score = len(questions)
|
||||
max_score = self.get_max_score(questions)
|
||||
|
||||
#cid_user_exam = exam.cid_user_exam.filter(user_user=user).first()
|
||||
#print(user)
|
||||
@@ -2314,6 +2330,18 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
template_context,
|
||||
)
|
||||
|
||||
def get_max_score(self, questions):
|
||||
match self.app_name:
|
||||
case "rapids" | "anatomy":
|
||||
max_score = len(questions) * 2
|
||||
case "longs":
|
||||
max_score = len(questions) * 8
|
||||
case "physics" | "shorts":
|
||||
max_score = len(questions) * 5
|
||||
case _:
|
||||
max_score = len(questions)
|
||||
return max_score
|
||||
|
||||
def exam_scores_all(self, request, pk):
|
||||
"""The exam scores pages. Displays all user scores in a tabular format.
|
||||
|
||||
@@ -2349,13 +2377,14 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
cached_scores = True
|
||||
|
||||
valid_cid_users = set(exam.valid_cid_users.all().values_list("cid", flat=True))
|
||||
print(valid_cid_users)
|
||||
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 = {}
|
||||
|
||||
if self.app_name in ("physics", "sbas", "longs"):
|
||||
if self.app_name in ("physics", "sbas", "longs", "shorts"):
|
||||
cached_scores = False
|
||||
questions = exam.get_questions()
|
||||
else:
|
||||
@@ -2420,7 +2449,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
callstate = s.get_answer_callstate()
|
||||
by_question[q][cid] = (ans, answer_score, callstate)
|
||||
user_answers_callstates[cid].append(callstate)
|
||||
elif self.app_name in ("anatomy", "sbas", "longs"):
|
||||
elif self.app_name in ("anatomy", "sbas", "longs", "shorts"):
|
||||
by_question[q][cid] = (ans, answer_score)
|
||||
else:
|
||||
zipped_ans_scores = zip(ans, answer_score)
|
||||
@@ -2442,13 +2471,13 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
by_question[question][user] = ("Not answered", 3.0)
|
||||
user_answers_marks[user].append(3.0)
|
||||
|
||||
if self.app_name in ("rapids", "anatomy", "sbas", "longs"):
|
||||
if self.app_name in ("physics",):
|
||||
user_scores[user] = sum(
|
||||
[i for i in user_answers_marks[user] if i != "unmarked"]
|
||||
[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"]
|
||||
[i for i in user_answers_marks[user] if i != "unmarked" and i != None]
|
||||
)
|
||||
|
||||
if self.app_name == "rapids":
|
||||
@@ -2463,12 +2492,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
# 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 = question_number * 2
|
||||
elif self.app_name == "physics":
|
||||
max_score = question_number * 5
|
||||
else:
|
||||
max_score = question_number
|
||||
max_score = self.get_max_score(questions)
|
||||
|
||||
if len(user_scores_list) < 1:
|
||||
mean = 0
|
||||
@@ -2842,6 +2866,7 @@ class CidUserExamView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||
|
||||
physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)]
|
||||
rapid_exams = [(i.name, i.pk) for i in RapidsExam.objects.filter(**filters)]
|
||||
shorts_exams = [(i.name, i.pk) for i in ShortsExam.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 LongsExam.objects.filter(**filters)]
|
||||
anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
|
||||
@@ -2856,6 +2881,7 @@ class CidUserExamView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||
|
||||
context["physics_exams"] = physics_exams
|
||||
context["rapid_exams"] = rapid_exams
|
||||
context["shorts_exams"] = shorts_exams
|
||||
context["sba_exams"] = sba_exams
|
||||
context["longs_exams"] = longs_exams
|
||||
context["anatomy_exams"] = anatomy_exams
|
||||
@@ -2880,6 +2906,7 @@ class CidUserView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||
|
||||
physics_exams = [(i.name, i.pk) for i in PhysicsExam.objects.filter(**filters)]
|
||||
rapid_exams = [(i.name, i.pk) for i in RapidsExam.objects.filter(**filters)]
|
||||
shorts_exams = [(i.name, i.pk) for i in ShortsExam.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 LongsExam.objects.filter(**filters)]
|
||||
anatomy_exams = [(i.name, i.pk) for i in AnatomyExam.objects.filter(**filters)]
|
||||
@@ -2890,6 +2917,7 @@ class CidUserView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||
|
||||
context["physics_exams"] = physics_exams
|
||||
context["rapid_exams"] = rapid_exams
|
||||
context["shorts_exams"] = shorts_exams
|
||||
context["sba_exams"] = sba_exams
|
||||
context["longs_exams"] = longs_exams
|
||||
context["anatomy_exams"] = anatomy_exams
|
||||
@@ -3343,6 +3371,7 @@ def manage_cid_users(request):
|
||||
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"))
|
||||
shorts_exams = json.loads(request.POST.get("shorts_exams"))
|
||||
casecollection_exams = json.loads(request.POST.get("casecollection_exams"))
|
||||
cid_groups = json.loads(request.POST.get("cid_groups"))
|
||||
|
||||
@@ -3404,6 +3433,7 @@ def manage_cid_users(request):
|
||||
c.sba_exams.clear()
|
||||
c.longs_exams.clear()
|
||||
c.anatomy_exams.clear()
|
||||
c.shorts_exams.clear()
|
||||
c.casecollection_exams.clear()
|
||||
return JsonResponse({"status": "success"})
|
||||
|
||||
@@ -3420,6 +3450,8 @@ def manage_cid_users(request):
|
||||
c.longs_exams.add(exam)
|
||||
for exam in anatomy_exams:
|
||||
c.anatomy_exams.add(exam)
|
||||
for exam in shorts_exams:
|
||||
c.shorts_exams.add(exam)
|
||||
for exam in casecollection_exams:
|
||||
c.casecollection_exams.add(exam)
|
||||
c.save()
|
||||
@@ -3433,6 +3465,7 @@ def manage_cid_users(request):
|
||||
c.sba_exams.set(sba_exams)
|
||||
c.longs_exams.set(longs_exams)
|
||||
c.anatomy_exams.set(anatomy_exams)
|
||||
c.shorts_exams.set(shorts_exams)
|
||||
c.casecollection_exams.set(casecollection_exams)
|
||||
c.save()
|
||||
return JsonResponse({"status": "success"})
|
||||
@@ -3479,6 +3512,8 @@ def manage_cid_users(request):
|
||||
c.longs_exams.set(longs_exams)
|
||||
if anatomy_exams:
|
||||
c.anatomy_exams.set(anatomy_exams)
|
||||
if shorts_exams:
|
||||
c.shorts_exams.set(shorts_exams)
|
||||
if casecollection_exams:
|
||||
c.casecollection_exams.set(casecollection_exams)
|
||||
c.save()
|
||||
@@ -3501,6 +3536,8 @@ def manage_cid_users(request):
|
||||
c.longs_exams.set(longs_exams)
|
||||
if anatomy_exams:
|
||||
c.anatomy_exams.set(anatomy_exams)
|
||||
if shorts_exams:
|
||||
c.shorts_exams.set(shorts_exams)
|
||||
if casecollection_exams:
|
||||
c.casecollection_exams.set(casecollection_exams)
|
||||
c.save()
|
||||
@@ -3832,6 +3869,7 @@ class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
||||
|
||||
GROUP_TYPES = (
|
||||
("Rapid Exams", "rapids_exams", RapidsExam),
|
||||
("Short Exams", "shorts_exams", ShortsExam),
|
||||
("Long Exams", "longs_exams", LongsExam),
|
||||
("SBA Exams", "sbas_exams", SbasExam),
|
||||
("Physic Exams", "physics_exams", PhysicsExam),
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
<a href="{% url 'longs:mark_answer_override' exam_id=exam.pk question_number=question_details.current|add:'-1' answer_id=answer.id %}">Set final score</a><br/>
|
||||
{% endif %}
|
||||
|
||||
Return to <a href="{% url 'longs:mark' exam.id question_details.current|add:'-1' %}">question
|
||||
overview</a>, <a href="{% url 'longs:mark_overview' pk=exam.pk %}">marking
|
||||
Return to <a class="marking-link" href="{% url 'longs:mark' exam.id question_details.current|add:'-1' %}">question
|
||||
overview</a>, <a class="marking-link" href="{% url 'longs:mark_overview' pk=exam.pk %}">marking
|
||||
overview</a><br />
|
||||
{% if previous_answer_id %}
|
||||
<a href="{% url 'longs:mark_answer' exam_id=exam.pk question_number=question_details.current|add:'-1' answer_id=previous_answer_id %}">Previous
|
||||
|
||||
@@ -679,7 +679,6 @@ def mark_answer(request, exam_id, question_number, answer_id, override=False):
|
||||
)
|
||||
|
||||
answer_list.sort()
|
||||
print(answer_list)
|
||||
|
||||
previous_answer_id = False
|
||||
next_answer_id = False
|
||||
@@ -690,7 +689,6 @@ def mark_answer(request, exam_id, question_number, answer_id, override=False):
|
||||
elif answer_list[-1] == answer_id:
|
||||
previous_answer_id = answer_list[-2]
|
||||
else:
|
||||
print(answer_list.index(answer_id))
|
||||
next_answer_id = answer_list[answer_list.index(answer_id) + 1]
|
||||
previous_answer_id = answer_list[answer_list.index(answer_id) - 1]
|
||||
|
||||
|
||||
@@ -1328,3 +1328,13 @@ span#user-id {
|
||||
opacity: 50%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.marking-link {
|
||||
color: #8600789e;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.marking-link:hover {
|
||||
color: #3282b8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
+132
-49
@@ -7,7 +7,12 @@ from atlas.models import CaseCollection, CidReportAnswer, Case as AtlasCase
|
||||
from generic.decorators import user_is_cid_user_manager
|
||||
from generic.filters import UserUserFilter
|
||||
from generic.tables import UserUserTable
|
||||
from generic.views import CidManagerRequiredMixin, RedirectMixin, get_question_and_content_type, get_user_exams
|
||||
from generic.views import (
|
||||
CidManagerRequiredMixin,
|
||||
RedirectMixin,
|
||||
get_question_and_content_type,
|
||||
get_user_exams,
|
||||
)
|
||||
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
@@ -63,10 +68,19 @@ from sbas.models import Exam as SbasExam
|
||||
|
||||
from anatomy.views import GenericExamViews as AnatomyExamViews
|
||||
from rapids.views import GenericExamViews as RapidsExamsViews
|
||||
from longs.views import GenericExamViews as LongsExamViews
|
||||
from shorts.views import GenericExamViews as ShortsExamsViews
|
||||
from longs.views import GenericExamViews as LongsExamsViews
|
||||
|
||||
from generic.forms import QuestionNoteForm
|
||||
from generic.models import CidUser, CimarCase, ExamCollection, QuestionNote, Supervisor, UserGrades, UserProfile
|
||||
from generic.models import (
|
||||
CidUser,
|
||||
CimarCase,
|
||||
ExamCollection,
|
||||
QuestionNote,
|
||||
Supervisor,
|
||||
UserGrades,
|
||||
UserProfile,
|
||||
)
|
||||
|
||||
from django_filters.views import FilterView
|
||||
|
||||
@@ -82,31 +96,44 @@ from loguru import logger
|
||||
|
||||
import psutil
|
||||
|
||||
|
||||
def feedback_checker(user):
|
||||
return user.groups.filter(name="feedback_checker").exists()
|
||||
|
||||
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def server(request):
|
||||
|
||||
return render(request, "server.html")
|
||||
|
||||
|
||||
@user_is_cid_user_manager
|
||||
def people(request):
|
||||
return render(request, "people.html", {})
|
||||
|
||||
|
||||
@login_required
|
||||
def profile(request):
|
||||
user = request.user
|
||||
return render(request, "profile.html", {"user": user})
|
||||
|
||||
|
||||
def index(request):
|
||||
collections = []
|
||||
|
||||
if request.user.is_authenticated:
|
||||
collections = ExamCollection.objects.filter(archive=False, author=request.user)
|
||||
|
||||
rcr_assessor = request.user.groups.filter(Q(name="rcr_radiology_assessor") | Q(name="rcr_oncology_assessor")| Q(name="rcr_assessor")).exists()
|
||||
return render(request, "index.html", {"rcr_assessor": rcr_assessor, "collections": collections})
|
||||
rcr_assessor = request.user.groups.filter(
|
||||
Q(name="rcr_radiology_assessor")
|
||||
| Q(name="rcr_oncology_assessor")
|
||||
| Q(name="rcr_assessor")
|
||||
).exists()
|
||||
return render(
|
||||
request,
|
||||
"index.html",
|
||||
{"rcr_assessor": rcr_assessor, "collections": collections},
|
||||
)
|
||||
|
||||
|
||||
@user_is_cid_user_manager
|
||||
def account_profile(request, slug):
|
||||
@@ -151,6 +178,7 @@ def cid_results(request, cid):
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def user_scores_admin(request, user_id):
|
||||
try:
|
||||
@@ -283,11 +311,15 @@ def active_exams(request, cid=None, passcode=None, based=True):
|
||||
rapid_exams = RapidsExamsViews.active_exams(
|
||||
request, False, cid=cid, passcode=passcode
|
||||
)
|
||||
long_exams = LongsExamViews.active_exams(request, False, cid=cid, passcode=passcode)
|
||||
short_exams = ShortsExamsViews.active_exams(
|
||||
request, False, cid=cid, passcode=passcode
|
||||
)
|
||||
long_exams = LongsExamsViews.active_exams(request, False, cid=cid, passcode=passcode)
|
||||
|
||||
exams = []
|
||||
exams.extend(anatomy_exams)
|
||||
exams.extend(rapid_exams)
|
||||
exams.extend(short_exams)
|
||||
exams.extend(long_exams)
|
||||
|
||||
active_exams["exams"] = exams
|
||||
@@ -318,7 +350,10 @@ def exam_submit(request):
|
||||
elif exam_type == "rapid":
|
||||
return RapidsExamsViews.post_exam_answers(request)
|
||||
elif exam_type == "long":
|
||||
return LongsExamViews.post_exam_answers(request)
|
||||
return LongsExamsViews.post_exam_answers(request)
|
||||
elif exam_type == "short":
|
||||
return ShortsExamsViews.post_exam_answers(request)
|
||||
|
||||
return JsonResponse({"success": False, "error": "Invalid exam type"})
|
||||
except ObjectDoesNotExist:
|
||||
return JsonResponse({"success": False, "error": "Invalid data"})
|
||||
@@ -512,12 +547,14 @@ def view_feedback(request):
|
||||
def privacy_view(request):
|
||||
return render(request, "privacy.html")
|
||||
|
||||
|
||||
def cimar(request):
|
||||
if "link_id" in request.GET:
|
||||
link_id = request.GET["link_id"]
|
||||
return render(request, "cimar.html", {"link_id": link_id})
|
||||
return render(request, "cimar.html")
|
||||
|
||||
|
||||
def about_view(request):
|
||||
return render(request, "about.html")
|
||||
|
||||
@@ -528,12 +565,17 @@ def stats(request):
|
||||
|
||||
anatomy_exam_count = AnatomyExam.objects.filter(archive=False).count()
|
||||
anatomy_question_count = AnatomyQuestion.objects.filter().count()
|
||||
return render(request, "stats.html"
|
||||
, {"collection_count": collection_count,
|
||||
"case_count": case_count,
|
||||
"anatomy_exam_count": anatomy_exam_count,
|
||||
"anatomy_question_count": anatomy_question_count
|
||||
})
|
||||
return render(
|
||||
request,
|
||||
"stats.html",
|
||||
{
|
||||
"collection_count": collection_count,
|
||||
"case_count": case_count,
|
||||
"anatomy_exam_count": anatomy_exam_count,
|
||||
"anatomy_question_count": anatomy_question_count,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# HTTP Error 400
|
||||
def page_not_found(request, exception):
|
||||
@@ -545,7 +587,6 @@ def page_not_found(request, exception):
|
||||
|
||||
|
||||
def page_forbidden(request, exception):
|
||||
|
||||
response = render(request, "403.html", context={"exception": exception})
|
||||
|
||||
response.status_code = 403
|
||||
@@ -575,11 +616,11 @@ class UserListTableView(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||
|
||||
filterset_class = UserUserFilter
|
||||
|
||||
#table_pagination = {"per_page": 5}
|
||||
# table_pagination = {"per_page": 5}
|
||||
|
||||
def get_paginate_by(self, table_data) -> int | None:
|
||||
return 100
|
||||
#return super().get_paginate_by(table_data)
|
||||
# return super().get_paginate_by(table_data)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@@ -620,7 +661,12 @@ class DeleteUserView(CidManagerRequiredMixin, DeleteView):
|
||||
|
||||
class UpdateUserView(CidManagerRequiredMixin, UpdateView):
|
||||
model = User
|
||||
fields = ["first_name", "last_name", "email", "is_active"] # Keep listing whatever fields
|
||||
fields = [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"is_active",
|
||||
] # Keep listing whatever fields
|
||||
# the combined UserProfile and User exposes.
|
||||
template_name = "user_update.html"
|
||||
slug_field = "username"
|
||||
@@ -649,21 +695,31 @@ class UpdateUserProfileAdminView(CidManagerRequiredMixin, UpdateView):
|
||||
# No need for reverse_lazy here, because it's called inside the method
|
||||
return reverse(view_name, kwargs={"slug": self.object.username})
|
||||
|
||||
|
||||
class UpdateUserProfileView(UpdateView):
|
||||
model = UserProfile
|
||||
fields = [
|
||||
"supervisor",
|
||||
"grade",
|
||||
"registration_number",
|
||||
#"peninsula_trainee",
|
||||
# "peninsula_trainee",
|
||||
] # Keep listing whatever fields
|
||||
template_name = "user_update_profile.html"
|
||||
slug_field = "user__username"
|
||||
slug_url_kwarg = "slug"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user.is_superuser or request.user.groups.filter(name="cid_user_manager").exists():
|
||||
self.fields = ["supervisor", "grade", "registration_number", "site", "peninsula_trainee"]
|
||||
if (
|
||||
request.user.is_superuser
|
||||
or request.user.groups.filter(name="cid_user_manager").exists()
|
||||
):
|
||||
self.fields = [
|
||||
"supervisor",
|
||||
"grade",
|
||||
"registration_number",
|
||||
"site",
|
||||
"peninsula_trainee",
|
||||
]
|
||||
# Check permissions for the request.user here
|
||||
elif request.user != self.get_object().user:
|
||||
raise Http404
|
||||
@@ -680,18 +736,22 @@ class UpdateUserProfileView(UpdateView):
|
||||
form = context["form"]
|
||||
|
||||
if "supervisor" in form.fields:
|
||||
#form.fields["supervisor"].queryset = Supervisor.objects.all()
|
||||
# form.fields["supervisor"].queryset = Supervisor.objects.all()
|
||||
form.fields["supervisor"] = forms.ModelChoiceField(
|
||||
Supervisor.objects.all().order_by("name"), required=False, widget=autocomplete.ModelSelect2(url='generic:supervisor-autocomplete')
|
||||
)
|
||||
Supervisor.objects.all().order_by("name"),
|
||||
required=False,
|
||||
widget=autocomplete.ModelSelect2(url="generic:supervisor-autocomplete"),
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
def get_success_url(self):
|
||||
if "redirect" in self.request.GET:
|
||||
return self.request.GET["redirect"]
|
||||
|
||||
if self.request.user.is_superuser or self.request.user.groups.filter(name="cid_user_manager").exists():
|
||||
if (
|
||||
self.request.user.is_superuser
|
||||
or self.request.user.groups.filter(name="cid_user_manager").exists()
|
||||
):
|
||||
view_name = "account_profile"
|
||||
# No need for reverse_lazy here, because it's called inside the method
|
||||
return reverse(view_name, kwargs={"slug": self.object.username})
|
||||
@@ -699,7 +759,6 @@ class UpdateUserProfileView(UpdateView):
|
||||
return reverse("profile")
|
||||
|
||||
|
||||
|
||||
# class UpdateUser(TemplateView):
|
||||
#
|
||||
# user_form_class = UserForm
|
||||
@@ -747,12 +806,12 @@ def accounts_check_users(request):
|
||||
|
||||
return
|
||||
|
||||
|
||||
@user_is_cid_user_manager
|
||||
def accounts_bulk_create_check(request):
|
||||
if not request.method == "POST":
|
||||
return HttpResponse("Invalid request")
|
||||
|
||||
|
||||
user = {}
|
||||
|
||||
user["email"] = request.POST.get("email")
|
||||
@@ -791,7 +850,9 @@ def accounts_bulk_create_check(request):
|
||||
errors.append("Supervisor name does not match")
|
||||
|
||||
if errors:
|
||||
return HttpResponse(format_html("<span class='error'>{}</span>", "<br/>".join(errors)))
|
||||
return HttpResponse(
|
||||
format_html("<span class='error'>{}</span>", "<br/>".join(errors))
|
||||
)
|
||||
|
||||
return HttpResponse(format_html("<span class='info'>{}</span>", "<br/>".join(info)))
|
||||
|
||||
@@ -864,7 +925,7 @@ def accounts_bulk_create(request):
|
||||
except Exception as error:
|
||||
error_text = (
|
||||
error_text
|
||||
+ f"<p>Error creating user: { user['email'] }<br/>{error}</p>"
|
||||
+ f"<p>Error creating user: {user['email']}<br/>{error}</p>"
|
||||
)
|
||||
# No need to try and create a profile if we can't create a user
|
||||
continue
|
||||
@@ -899,7 +960,7 @@ def accounts_bulk_create(request):
|
||||
new_user.delete()
|
||||
error_text = (
|
||||
error_text
|
||||
+ f"<p>Error creating user profile: { user['email'] }<br/>{error} (user not created)</p>"
|
||||
+ f"<p>Error creating user profile: {user['email']}<br/>{error} (user not created)</p>"
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
@@ -939,34 +1000,42 @@ def request_cid_details(request):
|
||||
def cimar_study_dicom_json(request, uuid="c9417b53-87aa-4c40-aca0-3fa34c225536"):
|
||||
with CimarAPI(request.user.userprofile.cimar_sid) as api:
|
||||
print(api.sid)
|
||||
#studies = api.list_studies()
|
||||
#test_study = studies["studies"][0]
|
||||
# studies = api.list_studies()
|
||||
# test_study = studies["studies"][0]
|
||||
|
||||
#schema = api.get_study_schema(test_study)
|
||||
# schema = api.get_study_schema(test_study)
|
||||
|
||||
schema = api.get_study_schema_by_uuid("c9417b53-87aa-4c40-aca0-3fa34c225536")
|
||||
|
||||
return JsonResponse(api.get_study_dicom_json(schema))
|
||||
|
||||
|
||||
def cimar_study_viewer(request, uuid="c9417b53-87aa-4c40-aca0-3fa34c225536"):
|
||||
case = get_object_or_404(CimarCase, uuid=uuid)
|
||||
|
||||
case = get_object_or_404(CimarCase, uuid=uuid)
|
||||
viewer_link = case.viewer_link + f"?sid={request.user.userprofile.cimar_sid}"
|
||||
return render(request, "cimar_embed.html", {"viewer_link": viewer_link})
|
||||
|
||||
viewer_link = case.viewer_link + f"?sid={request.user.userprofile.cimar_sid}"
|
||||
return render(request, "cimar_embed.html", {"viewer_link": viewer_link})
|
||||
|
||||
def cimar_study_viewer_embed(request, uuid="c9417b53-87aa-4c40-aca0-3fa34c225536"):
|
||||
case = get_object_or_404(CimarCase, uuid=uuid)
|
||||
|
||||
case = get_object_or_404(CimarCase, uuid=uuid)
|
||||
viewer_link = case.viewer_link + f"?sid={request.user.userprofile.cimar_sid}"
|
||||
return render(request, "cimar_embed.html#embed", {"viewer_link": viewer_link})
|
||||
|
||||
viewer_link = case.viewer_link + f"?sid={request.user.userprofile.cimar_sid}"
|
||||
return render(request, "cimar_embed.html#embed", {"viewer_link": viewer_link})
|
||||
|
||||
def cimar_study_series_block(request, uuid):
|
||||
case = get_object_or_404(CimarCase, uuid=uuid)
|
||||
|
||||
case = get_object_or_404(CimarCase, uuid=uuid)
|
||||
return render(
|
||||
request,
|
||||
"cimar_series.html",
|
||||
{
|
||||
"series_data": case.series_data["series"],
|
||||
"cimar_sid": request.user.userprofile.cimar_sid,
|
||||
},
|
||||
)
|
||||
|
||||
return render(request, "cimar_series.html", {"series_data": case.series_data["series"], "cimar_sid": request.user.userprofile.cimar_sid})
|
||||
|
||||
def cimar_status(request):
|
||||
login_status = CimarAPI(request.user.userprofile.cimar_sid).check_login_status()
|
||||
@@ -974,20 +1043,26 @@ def cimar_status(request):
|
||||
if login_status:
|
||||
return HttpResponse("")
|
||||
else:
|
||||
return render(request, "cimar_login.html#login-block", {"login_status": login_status, "cimar_sid": request.user.userprofile.cimar_sid})
|
||||
return render(
|
||||
request,
|
||||
"cimar_login.html#login-block",
|
||||
{
|
||||
"login_status": login_status,
|
||||
"cimar_sid": request.user.userprofile.cimar_sid,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def cimar_logout(request):
|
||||
CimarAPI(request.user.userprofile.cimar_sid).logout()
|
||||
return HttpResponse("Logged out")
|
||||
|
||||
def cimar_login(request):
|
||||
|
||||
def cimar_login(request):
|
||||
if request.htmx:
|
||||
username = request.POST.get("username")
|
||||
password = request.POST.get("password")
|
||||
|
||||
|
||||
|
||||
if not username and not password:
|
||||
return HttpResponse("Please fill in login details")
|
||||
|
||||
@@ -999,12 +1074,20 @@ def cimar_login(request):
|
||||
except InvalidCredentials:
|
||||
return HttpResponse("Invalid login details")
|
||||
|
||||
|
||||
return HttpResponse("Logged in")
|
||||
|
||||
else:
|
||||
try:
|
||||
login_status = CimarAPI(request.user.userprofile.cimar_sid).check_login_status()
|
||||
login_status = CimarAPI(
|
||||
request.user.userprofile.cimar_sid
|
||||
).check_login_status()
|
||||
except NoSession:
|
||||
login_status = False
|
||||
return render(request, "cimar_login.html", {"login_status": login_status, "cimar_sid": request.user.userprofile.cimar_sid})
|
||||
return render(
|
||||
request,
|
||||
"cimar_login.html",
|
||||
{
|
||||
"login_status": login_status,
|
||||
"cimar_sid": request.user.userprofile.cimar_sid,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixi
|
||||
|
||||
from shorts.models import (
|
||||
Abnormality,
|
||||
AnswerMarks,
|
||||
Examination,
|
||||
Question,
|
||||
QuestionFinding,
|
||||
@@ -256,3 +257,17 @@ class QuestionFindingForm(ModelForm):
|
||||
super(QuestionFindingForm, self).__init__(*args, **kwargs)
|
||||
|
||||
ModelForm.__init__(self, *args, **kwargs)
|
||||
|
||||
class MarkQuestionSingleForm(ModelForm):
|
||||
class Meta:
|
||||
model = UserAnswer
|
||||
fields = ["score", "candidate_feedback"]
|
||||
|
||||
widgets = {
|
||||
"candidate_feedback": Textarea(attrs={"rows": 3}),
|
||||
}
|
||||
|
||||
class MarkQuestionDoubleForm(ModelForm):
|
||||
class Meta:
|
||||
model = AnswerMarks
|
||||
fields = ["score", "mark_reason"]
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-14 12:55
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shorts', '0005_remove_question_abnormality_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='useranswer',
|
||||
name='score',
|
||||
field=models.IntegerField(blank=True, default=0, null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)]),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-28 08:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shorts', '0006_alter_useranswer_score'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='exam',
|
||||
name='double_mark',
|
||||
field=models.BooleanField(default=False, help_text='Defines if an exam is expected to be double marked'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-28 08:55
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shorts', '0007_exam_double_mark'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='useranswer',
|
||||
name='candidate_feedback',
|
||||
field=models.TextField(blank=True, help_text='Feedback for the candidate', null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AnswerMarks',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('score', models.IntegerField(blank=True, default=0, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(5)])),
|
||||
('mark_reason', models.TextField(blank=True, help_text='Reason for the given mark - not visible to candidates', null=True)),
|
||||
('marker', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shorts_answers', to=settings.AUTH_USER_MODEL)),
|
||||
('user_answer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mark', to='shorts.useranswer')),
|
||||
],
|
||||
),
|
||||
]
|
||||
+101
-9
@@ -30,6 +30,7 @@ from django.utils.html import mark_safe
|
||||
from rapids.models import Abnormality, Examination, Region
|
||||
|
||||
from helpers.images import get_image_hash, image_as_base64
|
||||
from django.utils.html import format_html
|
||||
|
||||
|
||||
def image_directory_path(instance, filename):
|
||||
@@ -63,6 +64,12 @@ class Question(QuestionBase):
|
||||
related_name="shorts_authored_questions",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "{}: {}".format(
|
||||
self.id,
|
||||
self.history,
|
||||
)
|
||||
|
||||
def get_app_name(self):
|
||||
return "shorts"
|
||||
|
||||
@@ -116,6 +123,52 @@ class Question(QuestionBase):
|
||||
def get_best_sample_answer(self):
|
||||
return self.sample_answers.filter(score__gt=0).order_by("-score").first()
|
||||
|
||||
def get_exams(self):
|
||||
e = self.exams.all().values_list("name", flat=True)
|
||||
exams = ", ".join(e)
|
||||
return exams
|
||||
|
||||
def get_unmarked_user_answer_string(self, exam_pk: int = None):
|
||||
unmarked_answers = self.get_unmarked_user_answers(exam_pk)
|
||||
|
||||
if not unmarked_answers:
|
||||
return "No answers to mark"
|
||||
return format_html(
|
||||
"<span class='warn'>{} answer unmarked:</span> {}".format(
|
||||
len(unmarked_answers), ", ".join(unmarked_answers)
|
||||
)
|
||||
)
|
||||
|
||||
def get_unmarked_user_answers(self, exam_pk: int | None = None, marker=None):
|
||||
if exam_pk is None:
|
||||
user_answer_queryset = self.cid_user_answers.all()
|
||||
else:
|
||||
user_answer_queryset = self.cid_user_answers.filter(exam__id=exam_pk)
|
||||
|
||||
unmarked_answers = user_answer_queryset.filter(score__isnull=True)
|
||||
|
||||
|
||||
return unmarked_answers
|
||||
|
||||
|
||||
def get_unmarked_user_answer_count(self, exam_pk=None, marker=None):
|
||||
if exam_pk is None:
|
||||
return self.cid_user_answers.all().count()
|
||||
|
||||
return len(self.get_unmarked_user_answers(exam_pk, marker=marker))
|
||||
|
||||
def get_user_answers(self, exam_pk=None, include_normal=True):
|
||||
if exam_pk is None:
|
||||
queryset = self.cid_user_answers.all()
|
||||
else:
|
||||
queryset = self.cid_user_answers.filter(exam__id=exam_pk)
|
||||
|
||||
if not include_normal:
|
||||
queryset = queryset.filter(normal=False)
|
||||
|
||||
user_answers = set([i.answer for i in queryset])
|
||||
|
||||
return user_answers
|
||||
|
||||
|
||||
class QuestionImage(models.Model):
|
||||
@@ -230,6 +283,10 @@ class Exam(ExamBase):
|
||||
related_name="shorts_exam_markers",
|
||||
)
|
||||
|
||||
double_mark = models.BooleanField(
|
||||
default=False, help_text="Defines if an exam is expected to be double marked"
|
||||
)
|
||||
|
||||
valid_cid_users = models.ManyToManyField(
|
||||
CidUser, blank=True, related_name="shorts_exams"
|
||||
)
|
||||
@@ -271,20 +328,16 @@ class Exam(ExamBase):
|
||||
return "shorts"
|
||||
|
||||
def get_exam_json(self, based=True):
|
||||
# TODO
|
||||
|
||||
return {}
|
||||
questions = self.get_questions()
|
||||
|
||||
exam_questions = defaultdict(dict)
|
||||
|
||||
exam_order = []
|
||||
|
||||
q: Rapid
|
||||
q: Question
|
||||
for q in questions:
|
||||
exam_order.append(q.id)
|
||||
|
||||
# Loop through rapidimage associations
|
||||
images = []
|
||||
feedback_images = []
|
||||
image_titles = []
|
||||
@@ -311,7 +364,7 @@ class Exam(ExamBase):
|
||||
"images": images,
|
||||
# "feedback_image": [],
|
||||
# "annotations": [str(q.image_annotations)],
|
||||
"type": "rapid",
|
||||
"type": "short",
|
||||
}
|
||||
|
||||
if not self.exam_mode:
|
||||
@@ -331,9 +384,9 @@ class Exam(ExamBase):
|
||||
# exam_questions[q.id]["feedback_image"] = feedback_images
|
||||
|
||||
exam_json = {
|
||||
"eid": "rapid/{}".format(self.id),
|
||||
"eid": "short/{}".format(self.id),
|
||||
"cached": False,
|
||||
"exam_type": "rapid",
|
||||
"exam_type": "short",
|
||||
"exam_name": self.name,
|
||||
"exam_mode": self.exam_mode,
|
||||
"exam_order": exam_order,
|
||||
@@ -343,6 +396,7 @@ class Exam(ExamBase):
|
||||
if self.time_limit:
|
||||
exam_json["exam_time"] = self.time_limit
|
||||
|
||||
print(exam_json)
|
||||
return exam_json
|
||||
|
||||
def get_absolute_url(self):
|
||||
@@ -373,8 +427,13 @@ class UserAnswer(UserAnswerBase):
|
||||
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
|
||||
)
|
||||
|
||||
# If score is null then the answer is unmarked
|
||||
score = models.IntegerField(
|
||||
default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)]
|
||||
default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)], null=True
|
||||
)
|
||||
|
||||
candidate_feedback = models.TextField(
|
||||
null=True, blank=True, help_text="Feedback for the candidate"
|
||||
)
|
||||
|
||||
class CallStateOptions(models.TextChoices):
|
||||
@@ -426,6 +485,15 @@ class UserAnswer(UserAnswerBase):
|
||||
def get_answer_score(self, cached=True):
|
||||
return self.score
|
||||
|
||||
def get_answer_string(self):
|
||||
return self.answer
|
||||
|
||||
def is_marked(self):
|
||||
if self.score is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class QuestionFinding(FindingBase):
|
||||
question = models.ForeignKey(
|
||||
@@ -439,3 +507,27 @@ class QuestionFinding(FindingBase):
|
||||
def __str__(self) -> str:
|
||||
findings = self.findings.all().values_list("name")
|
||||
return f"Findings: {self.question.id}/{findings}/{self.description}"
|
||||
|
||||
class AnswerMarks(models.Model):
|
||||
# Same as in UserAnswer except null is not allowed
|
||||
score = models.IntegerField(
|
||||
default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)]
|
||||
)
|
||||
|
||||
mark_reason = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Reason for the given mark - not visible to candidates",
|
||||
)
|
||||
|
||||
marker = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, related_name="shorts_answers", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
user_answer = models.ForeignKey(
|
||||
UserAnswer,
|
||||
related_name="mark",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends 'generic/exam_scores_base.html' %}
|
||||
|
||||
{% block table_answers %}
|
||||
{% for question in questions %}
|
||||
<tr>
|
||||
<td><a href="{% url 'shorts:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter}}</a>
|
||||
</td>
|
||||
{% comment %} {% for cid in cids %}
|
||||
<td class="anatomy-ans user-answer-score-{{score_by_question|get_item:question|get_item:cid}}" title="answer score: {{score_by_question|get_item:question|get_item:cid}}">{{ans_by_question|get_item:question|get_item:cid}}</td>
|
||||
{% endfor %} {% endcomment %}
|
||||
{% for cid in cids %}
|
||||
{% with by_question|get_item:question|get_item:cid as ans_score %}
|
||||
<td class="shorts-ans " title="{{ans_score.0}}">{{ans_score.1|default_if_none:"Unmarked"}}</td>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endblock table_answers %}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
{% extends 'shorts/exams.html' %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
<a href="{% url 'shorts:exam_question_detail' exam.id question_details.current %}" title="View the Question">View</a>
|
||||
<a href="{% url 'shorts:question_update' question.id %}" title="Edit the Question">Edit</a>
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="{% url 'admin:shorts_question_change' question.id %}" title="Edit the Question using the admin interface">Admin
|
||||
Edit</a> <a href="{% url 'admin:shorts_useranswer_change' answer.id %}"
|
||||
title="Edit the user answer using the admin interface">Admin
|
||||
Edit (user answer)</a>
|
||||
{% endif %}
|
||||
<h2>Marking question <a href="{% url 'shorts:mark' exam.id question_details.current|add:'-1' %}"
|
||||
title="View question answers">{{question_details.current}}</a> of {{question_details.total}}</h2>
|
||||
|
||||
{% if discrepancy_form %}
|
||||
<div class="alert alert-info sticky-alert" role="alert">
|
||||
<details open>
|
||||
<summary><h3>This answer has discrepant scores</h3></summary>
|
||||
You can set the overall score here.<br/>
|
||||
|
||||
<form method="POST" class="post-form">{% csrf_token %}
|
||||
<input type="hidden" name="form_id" value="discrepancy_form">
|
||||
{{ discrepancy_form.as_p }}
|
||||
<button type="submit" name="save" class="" title="Save score">Save Score</button>
|
||||
</form>
|
||||
|
||||
<h4>Marker scores and reason</h4>
|
||||
<ul>
|
||||
{% for mark_object in answer.mark.all %}
|
||||
<li>{{mark_object.marker}}: {{mark_object.score}}<br/>
|
||||
{{mark_object.mark_reason}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not next_unmarked_id and not unmarked %}
|
||||
<div class="alert alert-info sticky-alert" role="alert">Success! Marking question complete. <br />
|
||||
|
||||
|
||||
{% if exam.double_mark %}
|
||||
<a href="{% url 'shorts:mark_answer_override' exam_id=exam.pk question_number=question_details.current|add:'-1' answer_id=answer.id %}">Set final score</a><br/>
|
||||
{% endif %}
|
||||
|
||||
Return to <a class="marking-link" href="{% url 'shorts:mark' exam.id question_details.current|add:'-1' %}">question
|
||||
overview</a>, <a class="marking-link" href="{% url 'shorts:mark_overview' pk=exam.pk %}">marking
|
||||
overview</a><br />
|
||||
{% if previous_answer_id %}
|
||||
<a href="{% url 'shorts:mark_answer' exam_id=exam.pk question_number=question_details.current|add:'-1' answer_id=previous_answer_id %}">Previous
|
||||
candidate</a>
|
||||
{% endif %}
|
||||
{% if next_answer_id %}
|
||||
<a href="{% url 'shorts:mark_answer' exam_id=exam.pk question_number=question_details.current|add:'-1' answer_id=next_answer_id %}">Next
|
||||
candidate</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<span>Marking Candidate: {{answer.get_candidate_masked}} ({{unmarked|length}} answer(s) left to mark)</span>
|
||||
<details closed>
|
||||
<summary title="click to view/hide the question details">Question Details</summary>
|
||||
<p class="pre-whitespace"><b>History:</b> {{ question.history }}</p>
|
||||
<p class="pre-whitespace"><b>Region:</b> {{ question.get_regions }}</p>
|
||||
<p class="pre-whitespace"><b>Examination:</b> {{ question.get_examinations }}</p>
|
||||
<p class="pre-whitespace"><b>Abnormality:</b> {{ question.get_abnormalities }}</p>
|
||||
<div class="pre-whitespace multi-image-block"><b>Images:</b>
|
||||
{% for image in question.images.all %}
|
||||
<span class="image-block">
|
||||
Image {{ forloop.counter }}{% if image.description %} ({{image.description}}){% endif %}{% if image.feedback_image %} [feedback image]{% endif %}:
|
||||
<div class="dicom-image shorts-img {% if image.feedback_image %}feedback-img{% endif %}"
|
||||
data-url="{{ remote_url }}{{ image.image.url}}"></div>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
||||
</details>
|
||||
<div class="marking-block">
|
||||
<details open>
|
||||
<summary title="click to view/hide the mark scheme">Marking Guidance</summary>
|
||||
{{ question.marking_guidance | safe}}
|
||||
</details>
|
||||
<details open>
|
||||
<summary title="click to view/hide user answers">User answer</summary>
|
||||
<div>
|
||||
<div class="shorts-answer">
|
||||
<pre>{{answer.answer}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="marking">
|
||||
<form method="POST" class="post-form">{% csrf_token %}
|
||||
<input type="hidden" name="form_id" value="mark_form">
|
||||
{{ form|crispy }}
|
||||
<button type="submit" name="save" class="save btn btn-default" title="Save answer">Save</button>
|
||||
{% if next_unmarked_id %}
|
||||
<button type="submit" name="next" class="save btn btn-default"
|
||||
title="Save answer and move to next question">Next answer</button>
|
||||
<!-- <button type="submit" name="skip" class="save btn btn-default">Skip</button> -->
|
||||
{% else %}
|
||||
{% if not unmarked %}
|
||||
Success! Marking question complete. <a href="{% url 'shorts:mark_overview' pk=exam.pk %}">Return to marking
|
||||
overview</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% if previous_answer_id %}
|
||||
<a href="{% url 'shorts:mark_answer' exam_id=exam.pk question_number=question_details.current|add:'-1' answer_id=previous_answer_id %}">Previous
|
||||
candidate</a>
|
||||
{% endif %}
|
||||
{% if next_answer_id %}
|
||||
<a href="{% url 'shorts:mark_answer' exam_id=exam.pk question_number=question_details.current|add:'-1' answer_id=next_answer_id %}">Next candidate</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends 'shorts/exams.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="shorts">
|
||||
<h2>Marking exam: {{ exam }}</h2>
|
||||
You can start marking from a particular question by clicking on it below.
|
||||
|
||||
<div>
|
||||
<button class="show-all-button">Show all</button><button class="show-unmarked-button">Show unmarked</button>
|
||||
</div>
|
||||
<div id="stark-marking-button"><a href="{% url 'shorts:mark' exam_pk=exam.pk sk=0 %}"><button>Click here to start marking</button></a></div>
|
||||
|
||||
<ul id="question-mark-list">
|
||||
{% for question, unmarked_count, unmarked_count2 in question_unmarked_map %}
|
||||
<li data-markcount={{unmarked_count}} {% if unmarked_count %}class="unmarked" {% endif %}><a href="{% url 'shorts:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter }}:
|
||||
{{ question }}</a><br />Unmarked answers: {{unmarked_count}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends 'longs/exams.html' %}
|
||||
|
||||
{% block content %}
|
||||
Question -> <a href="{% url 'longs:exam_question_detail' exam.id question_details.current|add:'-1' %}"
|
||||
title="View the Question">View</a>
|
||||
<a href="{% url 'longs:long_update' question.id %}" title="Edit the Question">Edit</a> <a
|
||||
href="{% url 'admin:longs_long_change' question.id %}" title="Edit the Question using the admin interface">Admin
|
||||
Edit</a>
|
||||
<h2>Marking question {{question_details.current}} of {{question_details.total}}</h2>
|
||||
<div>
|
||||
|
||||
{% if not unmarked_count %}
|
||||
<div class="alert alert-info" role="alert">All answers completely marked.</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">{{unmarked_count}} answer(s) incompletely marked.<br/>
|
||||
You have {{marker_unmarked_count}} / {{user_answers.count}} answers left to mark.
|
||||
</div>
|
||||
{% endif %}
|
||||
Answers:
|
||||
<ul>
|
||||
{% for answer in user_answers %}
|
||||
<li>
|
||||
<a href="{% url 'longs:mark_answer' exam.id question_details.current|add:'-1' answer.id %}"
|
||||
title="Click to mark"> {{answer.cid}}</a>:
|
||||
Score {{answer.get_answer_score}} [Markers: {{answer.get_markers}}] [Scores: {{answer.get_mark_scores}}]
|
||||
{% if answer.discrepant_answers %}
|
||||
This answer has discrepant scores.
|
||||
{% endif %}
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div>
|
||||
{% if question_details.current > 1 %}
|
||||
<a href="{% url 'longs:mark' exam.id question_details.current|add:'-2' %}"
|
||||
title="Previous question">Previous</a>
|
||||
{% endif %}
|
||||
{% if question_details.current >= question_details.total %}
|
||||
{% else %}
|
||||
<a href="{% url 'longs:mark' exam.id question_details.current %}" title="Next question">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends 'shorts/exams.html' %}
|
||||
|
||||
{% block content %}
|
||||
Question -> <a href="{% url 'shorts:exam_question_detail' exam.id question_details.current|add:'-1' %}"
|
||||
title="View the Question">View</a>
|
||||
<a href="{% url 'shorts:question_update' question.id %}" title="Edit the Question">Edit</a> <a
|
||||
href="{% url 'admin:shorts_question_change' question.id %}" title="Edit the Question using the admin interface">Admin
|
||||
Edit</a>
|
||||
<h2>Marking question {{question_details.current}} of {{question_details.total}}</h2>
|
||||
<div>
|
||||
|
||||
{% if not unmarked_count %}
|
||||
<div class="alert alert-info" role="alert">No answers to mark.</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning" role="alert">{{unmarked_count}} answer(s) to mark.</div>
|
||||
|
||||
{% endif %}
|
||||
Answers:
|
||||
<ul>
|
||||
{% for answer in user_answers %}
|
||||
<li>
|
||||
<a class="mark-answer-link" href="{% url 'shorts:mark_answer' exam.id question_details.current|add:'-1' answer.id %}"
|
||||
title="Click to mark"> {{answer.get_candidate_masked}}</a>:
|
||||
{% if answer.is_marked %}
|
||||
Score {{answer.get_answer_score}}
|
||||
{% else %}
|
||||
Unmarked
|
||||
{% endif %}
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div>
|
||||
{% if question_details.current > 1 %}
|
||||
<a href="{% url 'shorts:mark' exam.id question_details.current|add:'-2' %}"
|
||||
title="Previous question">Previous</a>
|
||||
{% endif %}
|
||||
{% if question_details.current >= question_details.total %}
|
||||
{% else %}
|
||||
<a href="{% url 'shorts:mark' exam.id question_details.current %}" title="Next question">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -48,6 +48,16 @@ urlpatterns = [
|
||||
),
|
||||
#path("answer/<int:answer_id>/confirm", views.confirm_answer, name="confirm_answer"),
|
||||
#path("answer/<int:answer_id>/delete", views.delete_answer, name="delete_answer"),
|
||||
path(
|
||||
"exam/<int:exam_id>/<int:question_number>/<int:answer_id>/mark",
|
||||
views.mark_answer,
|
||||
name="mark_answer",
|
||||
),
|
||||
path(
|
||||
"exam/<int:exam_id>/<int:question_number>/<int:answer_id>/mark_override",
|
||||
views.mark_answer_override,
|
||||
name="mark_answer_override",
|
||||
),
|
||||
path("exam/<int:exam_pk>/<int:sk>/mark", views.mark, name="mark"),
|
||||
path("exam/<int:exam_pk>/<int:sk>/mark/all", views.mark_all, name="mark_all"),
|
||||
path(
|
||||
|
||||
+234
-129
@@ -5,7 +5,11 @@ from django import forms
|
||||
# from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.exceptions import (
|
||||
ObjectDoesNotExist,
|
||||
PermissionDenied,
|
||||
ViewDoesNotExist,
|
||||
)
|
||||
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
@@ -41,6 +45,7 @@ from atlas.models import Finding, Structure, Condition
|
||||
from shorts.decorators import user_is_author_or_shorts_checker
|
||||
|
||||
from .models import (
|
||||
AnswerMarks,
|
||||
Exam,
|
||||
Question,
|
||||
QuestionFinding,
|
||||
@@ -55,6 +60,8 @@ from .forms import (
|
||||
ExamAuthorForm,
|
||||
ExamGroupsForm,
|
||||
ExamMarkerForm,
|
||||
MarkQuestionDoubleForm,
|
||||
MarkQuestionSingleForm,
|
||||
QuestionFindingForm,
|
||||
QuestionForm,
|
||||
QuestionSampleAnswerForm,
|
||||
@@ -342,11 +349,206 @@ def mark_all(request, exam_pk, sk):
|
||||
def mark_review(request, exam_pk, sk):
|
||||
return mark(request, exam_pk, sk, unmarked_exam_answers_only=False, review=True)
|
||||
|
||||
def mark_answer_override(request, exam_id, question_number, cid):
|
||||
return mark_answer(request, exam_id, question_number, cid, override=True)
|
||||
|
||||
|
||||
def mark_answer(request, exam_id, question_number, answer_id, override=False):
|
||||
exam = get_object_or_404(Exam, pk=exam_id)
|
||||
|
||||
if not GenericExamViews.check_user_marker_access(request.user, exam_id):
|
||||
raise PermissionDenied
|
||||
|
||||
questions = exam.get_questions()
|
||||
|
||||
n = question_number
|
||||
|
||||
question_details = {
|
||||
"total": len(questions),
|
||||
"current": n + 1,
|
||||
}
|
||||
|
||||
try:
|
||||
question = questions[question_number]
|
||||
except IndexError:
|
||||
raise Http404("Exam question does not exist")
|
||||
|
||||
try:
|
||||
answer = question.cid_user_answers.get(pk=answer_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404("User answer does not exist")
|
||||
|
||||
answer_list = list(
|
||||
question.cid_user_answers.filter(exam__id=exam_id).values_list("id", flat=True)
|
||||
)
|
||||
|
||||
answer_list.sort()
|
||||
|
||||
previous_answer_id = False
|
||||
next_answer_id = False
|
||||
|
||||
if len(answer_list) > 1:
|
||||
if answer_list[0] == answer_id:
|
||||
next_answer_id = answer_list[1]
|
||||
elif answer_list[-1] == answer_id:
|
||||
previous_answer_id = answer_list[-2]
|
||||
else:
|
||||
print(answer_list.index(answer_id))
|
||||
next_answer_id = answer_list[answer_list.index(answer_id) + 1]
|
||||
previous_answer_id = answer_list[answer_list.index(answer_id) - 1]
|
||||
|
||||
try:
|
||||
if exam.double_mark:
|
||||
unmarked = question.get_unmarked_user_answers(
|
||||
exam_pk=exam.id, marker=request.user
|
||||
)
|
||||
else:
|
||||
unmarked = question.get_unmarked_user_answers(exam_pk=exam.id)
|
||||
next_unmarked_id = unmarked[0].id
|
||||
if next_unmarked_id == answer_id:
|
||||
next_unmarked_id = unmarked[1].id
|
||||
except IndexError:
|
||||
next_unmarked_id = False
|
||||
|
||||
# We use different forms if the exam should be single or double marked
|
||||
|
||||
if not exam.double_mark or (
|
||||
request.method == "POST" and request.POST["form_id"] == "discrepancy_form"
|
||||
):
|
||||
if request.method == "POST":
|
||||
form = MarkQuestionSingleForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
# If skip button is pressed skip the question without marking
|
||||
# Skip is problematic if we skip to the next unmarked answer
|
||||
# if "skip" in request.POST:
|
||||
# return redirect("longs:mark_answer", pk=exam_id, sk=question_number, cid=next_unmarked_id)
|
||||
|
||||
# Extract score from form and save it to the object
|
||||
answer.score = form.cleaned_data["score"]
|
||||
answer.candidate_feedback = form.cleaned_data["candidate_feedback"]
|
||||
answer.save()
|
||||
|
||||
if "next" in request.POST:
|
||||
return redirect(
|
||||
"shorts:mark_answer",
|
||||
exam_id=exam_id,
|
||||
question_number=question_number,
|
||||
answer_id=next_unmarked_id,
|
||||
)
|
||||
if "save" in request.POST:
|
||||
return redirect(
|
||||
"shorts:mark_answer",
|
||||
exam_id=exam_id,
|
||||
question_number=question_number,
|
||||
answer_id=answer_id,
|
||||
)
|
||||
# elif "previous" in request.POST:
|
||||
# return redirect("longs:mark", pk=exam_id, sk=n - 1)
|
||||
|
||||
else:
|
||||
form = MarkQuestionSingleForm(
|
||||
initial={
|
||||
"score": answer.score,
|
||||
"candidate_feedback": answer.candidate_feedback,
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
if request.method == "POST":
|
||||
form = MarkQuestionDoubleForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
# If skip button is pressed skip the question without marking
|
||||
# Skip is problematic if we skip to the next unmarked answer
|
||||
# if "skip" in request.POST:
|
||||
# return redirect("longs:mark_answer", pk=exam_id, sk=question_number, cid=next_unmarked_id)
|
||||
|
||||
mark_object = AnswerMarks.objects.get_or_create(
|
||||
user_answer=answer, marker=request.user
|
||||
)[0]
|
||||
# Extract score from form and save it to the object
|
||||
mark_object.score = form.cleaned_data["score"]
|
||||
mark_object.mark_reason = form.cleaned_data["mark_reason"]
|
||||
mark_object.candidate_feedback = form.cleaned_data["candidate_feedback"]
|
||||
mark_object.marker = request.user
|
||||
mark_object.save()
|
||||
|
||||
# Form is saved, check if we have two marks (and if they match)
|
||||
if answer.mark.count() == 2:
|
||||
# if they do automatically update teh scoer
|
||||
marks = set(answer.mark.values_list("score", flat=True))
|
||||
if len(marks) == 1:
|
||||
answer.score = marks.pop()
|
||||
else:
|
||||
answer.score = ""
|
||||
answer.save()
|
||||
|
||||
if "next" in request.POST:
|
||||
return redirect(
|
||||
"shorts:mark_answer",
|
||||
exam_id=exam_id,
|
||||
question_number=question_number,
|
||||
answer_id=next_unmarked_id,
|
||||
)
|
||||
if "save" in request.POST:
|
||||
return redirect(
|
||||
"shorts:mark_answer",
|
||||
exam_id=exam_id,
|
||||
question_number=question_number,
|
||||
answer_id=answer_id,
|
||||
)
|
||||
# elif "previous" in request.POST:
|
||||
# return redirect("longs:mark", pk=exam_id, sk=n - 1)
|
||||
|
||||
else:
|
||||
try:
|
||||
mark_object = AnswerMarks.objects.get(
|
||||
user_answer=answer, marker=request.user
|
||||
)
|
||||
form = MarkQuestionDoubleForm(
|
||||
initial={
|
||||
"score": mark_object.score,
|
||||
"mark_reason": mark_object.mark_reason,
|
||||
"candidate_feedback": mark_object.candidate_feedback,
|
||||
}
|
||||
)
|
||||
except AnswerMarks.DoesNotExist:
|
||||
form = MarkQuestionDoubleForm()
|
||||
|
||||
discrepancy_form = False
|
||||
if answer.mark.count() == 2 or override:
|
||||
# if they do automatically update teh scoer
|
||||
marks = set(answer.mark.values_list("score", flat=True))
|
||||
if len(marks) > 1 or override:
|
||||
discrepancy_form = MarkQuestionSingleForm(
|
||||
initial={
|
||||
"score": answer.score,
|
||||
"candidate_feedback": answer.candidate_feedback,
|
||||
}
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"shorts/mark_answer.html",
|
||||
{
|
||||
"exam": exam,
|
||||
"form": form,
|
||||
"discrepancy_form": discrepancy_form,
|
||||
"answer": answer,
|
||||
"question": question,
|
||||
"question_details": question_details,
|
||||
"next_unmarked_id": next_unmarked_id,
|
||||
"unmarked": unmarked,
|
||||
"previous_answer_id": previous_answer_id,
|
||||
"next_answer_id": next_answer_id,
|
||||
"answer_id": answer_id,
|
||||
},
|
||||
)
|
||||
|
||||
# @user_passes_test(user_is_admin, login_url="/accounts/login")
|
||||
@login_required
|
||||
def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
|
||||
return HttpResponse("Not implemented")
|
||||
def mark(request, exam_pk, sk, unmarked_exam_answers_only=True):
|
||||
exam = get_object_or_404(Exam, pk=exam_pk)
|
||||
|
||||
if not GenericExamViews.check_user_marker_access(request.user, exam_pk):
|
||||
@@ -355,12 +557,6 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
|
||||
if not exam.exam_mode:
|
||||
raise Http404("Packet not in exam mode")
|
||||
|
||||
mark_url = "shorts:mark"
|
||||
if review:
|
||||
mark_url = "shorts:mark_review"
|
||||
elif not unmarked_exam_answers_only:
|
||||
mark_url = "shorts:mark_all"
|
||||
|
||||
questions = exam.get_questions()
|
||||
|
||||
n = sk
|
||||
@@ -375,129 +571,38 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
|
||||
except IndexError:
|
||||
raise Http404("Exam question does not exist")
|
||||
|
||||
if request.method == "POST":
|
||||
user_answers = question.cid_user_answers.filter(exam__id=exam_pk)
|
||||
|
||||
form = MarkQuestionForm(request.POST)
|
||||
# *******************************
|
||||
# TODO: convert to JSON request
|
||||
# *******************************
|
||||
if form.is_valid():
|
||||
# If skip button is pressed skip the question without marking
|
||||
if "skip" in request.POST:
|
||||
return redirect(mark_url, exam_pk=exam_pk, sk=n + 1)
|
||||
unmarked_count = user_answers.filter(score__isnull=True).count()
|
||||
|
||||
cd = form.cleaned_data
|
||||
|
||||
marked_answers = json.loads(cd.get("marked_answers"))
|
||||
|
||||
# This should probably use json (or something else...)
|
||||
# ajax.....
|
||||
to_run = (
|
||||
("correct", Answer.MarkOptions.CORRECT),
|
||||
("half-correct", Answer.MarkOptions.HALF_MARK),
|
||||
("incorrect", Answer.MarkOptions.INCORRECT),
|
||||
)
|
||||
|
||||
for status_string, status in to_run:
|
||||
for ans in marked_answers[status_string]:
|
||||
ans = ans.strip()
|
||||
if ans == "":
|
||||
continue
|
||||
|
||||
a = Answer.objects.filter(
|
||||
answer__iexact=ans, question_id=question.pk
|
||||
).first()
|
||||
|
||||
if a is None:
|
||||
a = Answer()
|
||||
a.question_id = question.pk
|
||||
a.answer = ans
|
||||
|
||||
a.status = status
|
||||
a.save()
|
||||
|
||||
if "next" in request.POST:
|
||||
return redirect(mark_url, exam_pk=exam_pk, sk=n + 1)
|
||||
elif "previous" in request.POST:
|
||||
return redirect(mark_url, exam_pk=exam_pk, sk=n - 1)
|
||||
|
||||
# Reset user answers (relies on the functional javascript)
|
||||
unmarked_user_answers = set()
|
||||
if exam.double_mark:
|
||||
marker_unmarked_count = question.get_unmarked_user_answer_count(
|
||||
exam.pk, marker=request.user
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"shorts/mark_question_double_overview.html",
|
||||
{
|
||||
"exam": exam,
|
||||
"question": question,
|
||||
"question_details": question_details,
|
||||
"user_answers": user_answers,
|
||||
"unmarked_count": unmarked_count,
|
||||
"marker_unmarked_count": marker_unmarked_count,
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = MarkQuestionForm()
|
||||
|
||||
# answers_dict = {}
|
||||
|
||||
# for ans in question.answers.all():
|
||||
# answers_dict[ans.answer_compare] = ans
|
||||
|
||||
correct_answers = []
|
||||
half_mark_answers = []
|
||||
incorrect_answers = []
|
||||
review_user_answers = []
|
||||
unmarked_answers_bool = False
|
||||
unmarked_user_answers = []
|
||||
# this could be improved
|
||||
if question.normal:
|
||||
incorrect_answers = question.get_user_answers(exam_pk=exam_pk)
|
||||
if "" in incorrect_answers:
|
||||
incorrect_answers.remove("")
|
||||
if "normal" in incorrect_answers:
|
||||
incorrect_answers.remove("normal")
|
||||
else:
|
||||
if review:
|
||||
unmarked_user_answers = question.get_user_answers(
|
||||
exam_pk=exam_pk, include_normal=False
|
||||
)
|
||||
elif unmarked_exam_answers_only:
|
||||
unmarked_user_answers = question.get_unmarked_user_answers(exam_pk=exam_pk)
|
||||
else:
|
||||
unmarked_user_answers = question.get_unmarked_user_answers()
|
||||
|
||||
if not unmarked_exam_answers_only:
|
||||
correct_answers = question.answers.filter(status=Answer.MarkOptions.CORRECT)
|
||||
half_mark_answers = question.answers.filter(
|
||||
status=Answer.MarkOptions.HALF_MARK
|
||||
)
|
||||
incorrect_answers = question.answers.filter(
|
||||
status=Answer.MarkOptions.INCORRECT
|
||||
)
|
||||
|
||||
if review:
|
||||
for ans in unmarked_user_answers:
|
||||
# duplicated from user answer model....
|
||||
marked_ans = question.answers.filter(answer_compare__iexact=ans).first()
|
||||
mark = "unmarked"
|
||||
if marked_ans is not None:
|
||||
if marked_ans.status == Answer.MarkOptions.CORRECT:
|
||||
mark = "correct"
|
||||
elif marked_ans.status == Answer.MarkOptions.HALF_MARK:
|
||||
mark = "half-correct"
|
||||
elif marked_ans.status == Answer.MarkOptions.INCORRECT:
|
||||
mark = "incorrect"
|
||||
if mark == "unmarked":
|
||||
unmarked_answers_bool = True
|
||||
review_user_answers.append((ans, mark))
|
||||
|
||||
return render(
|
||||
request,
|
||||
"shorts/mark.html",
|
||||
{
|
||||
"exam": exam,
|
||||
"form": form,
|
||||
"review": review,
|
||||
"question": question,
|
||||
"question_number": n,
|
||||
"question_details": question_details,
|
||||
"unmarked_answers_bool": unmarked_answers_bool,
|
||||
"user_answers": unmarked_user_answers,
|
||||
"review_user_answers": review_user_answers,
|
||||
"correct_answers": correct_answers,
|
||||
"half_mark_answers": half_mark_answers,
|
||||
"incorrect_answers": incorrect_answers,
|
||||
"unmarked_exam_answers_only": unmarked_exam_answers_only,
|
||||
},
|
||||
)
|
||||
return render(
|
||||
request,
|
||||
"shorts/mark_question_single_overview.html",
|
||||
{
|
||||
"exam": exam,
|
||||
"question": question,
|
||||
"question_details": question_details,
|
||||
"user_answers": user_answers,
|
||||
"unmarked_count": unmarked_count,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView):
|
||||
|
||||
Reference in New Issue
Block a user