shorts might now be functional?

This commit is contained in:
Ross
2025-04-28 12:15:31 +01:00
parent 405b17776f
commit 06e66ad52c
18 changed files with 901 additions and 234 deletions
@@ -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
View File
@@ -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),
+2 -2
View File
@@ -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
-2
View File
@@ -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]
+10
View File
@@ -1327,4 +1327,14 @@ a, a:link {
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;
}
+138 -55
View File
@@ -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,10 +850,12 @@ 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)))
@user_is_cid_user_manager
def accounts_bulk_create(request):
@@ -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)
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(request, uuid="c9417b53-87aa-4c40-aca0-3fa34c225536"):
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})
def cimar_study_viewer_embed(request, uuid="c9417b53-87aa-4c40-aca0-3fa34c225536"):
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})
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})
def cimar_study_series_block(request, 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,
},
)
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})
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,
},
)
+16 -1
View File
@@ -11,6 +11,7 @@ from generic.forms import ExamAuthorFormMixin, ExamFormMixin, ExamGroupsFormMixi
from shorts.models import (
Abnormality,
AnswerMarks,
Examination,
Question,
QuestionFinding,
@@ -255,4 +256,18 @@ class QuestionFindingForm(ModelForm):
super(QuestionFindingForm, self).__init__(*args, **kwargs)
ModelForm.__init__(self, *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
View File
@@ -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,
)
+19
View File
@@ -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 %}
+118
View File
@@ -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 %}
+10
View File
@@ -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
View File
@@ -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):