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
+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):