This commit is contained in:
Ross
2021-09-25 19:20:47 +01:00
parent c93e5d4a10
commit 0cb4294427
6 changed files with 280 additions and 92 deletions
+44 -1
View File
@@ -213,6 +213,49 @@ class AnatomyQuestion(models.Model):
def get_annotations(self): def get_annotations(self):
return self.image_annotations return self.image_annotations
def get_title(self):
return "{}".format(self.description)
def get_correct_unstripped_answers(self):
return set(
[
str(i)
for i in self.answers.filter(proposed=False)
if i.status == i.MarkOptions.CORRECT
]
)
def get_question_json(self, based=True):
"""Returns a json representation of the question"""
# Loop through rapidimage associations
images = []
annotations = []
feedback_images = []
if based:
images.append(image_as_base64(self.image))
else:
images.append("{}/{}".format(settings.REMOTE_URL, self.image.url))
if self.image_annotations:
annotations.append(str(self.image_annotations))
json = {
"images": images,
#"feedback_image": [],
"annotations": annotations,
"type": "anatomy",
"title": self.get_title(),
"question": str(self.question_type),
}
json["answers"] = list(self.get_correct_unstripped_answers())
return json
@reversion.register @reversion.register
class Answer(models.Model): class Answer(models.Model):
@@ -313,7 +356,7 @@ class Exam(ExamBase):
exam_order.append(q.id) exam_order.append(q.id)
exam_questions[q.id] = { exam_questions[q.id] = {
"title": "{}".format(q.description), "title": q.get_title(),
"question": str(q.question_type), "question": str(q.question_type),
"images": [image_as_base64(q.image)], "images": [image_as_base64(q.image)],
"annotations": [str(q.image_annotations)], "annotations": [str(q.image_annotations)],
+65 -14
View File
@@ -1,18 +1,69 @@
{% extends 'anatomy/base.html' %} {% extends 'anatomy/base.html' %}
{% block content %} {% block content %}
<div class="anatomy"> <div class="question-display-block">
<h2>Exam: {{ exam.name }}</h2> Question <span class="question-number">1</span>
<h3>Candidate: {{ cid }}</h3> <div id="single-dicom-viewer" class="dicom-viewer" data-images="" data-annotations=''>
Answers: </div>
<ul>{% for ans, score, correct_answer in answers_and_marks %} <div class="answers">Answers: </div>
<li class="user-answer-li">Question {{forloop.counter}} - Correct answer: <span class="correct-answer">{{ correct_answer }}</span></li>
<span class="user-answer-score user-answer-score-{{score}}"><pre>{{ans}}</pre> ({{score}})</span>
{% endfor %}
</ul>
<br /> Total mark: {{ total_score }} / {{max_score}}
<div>
<a href="{% url 'cid_scores' cid %}">Other exams</a>
</div> </div>
</div> <div class="anatomy">
{% endblock %} <h2>Exam: {{ exam.name }}</h2>
<h3>Candidate: {{ cid }}</h3>
Answers:
<ul class="score-answer-list">{% for ans, score, correct_answer in answers_and_marks %}
<li class="user-answer-li">Question {{forloop.counter}} - Correct answer: <span
class="correct-answer">{{ correct_answer }}</span></li>
<span class="user-answer-score user-answer-score-{{score}} rapid-ans">
<pre>{{ans}}</pre> ({{score}})
</span>
<span class="view-question-link" data-qn={{forloop.counter0}}>View</span>
{% endfor %}
</ul>
<br /> Total mark: {{ total_score }} / {{max_score}}
<div>
<a href="{% url 'cid_scores' cid %}">Other exams</a>
</div>
</div>
{% endblock %}
{% block js %}
<script>
$(document).ready(function () {
$(".view-question-link").on("click", function (e) {
console.log("click", e)
question_number = e.currentTarget.dataset.qn;
$("#single-dicom-viewer").empty();
$.ajax({
url: `{% url 'anatomy:question_review' pk=exam.pk %}`,
data: {
csrfmiddlewaretoken: "{{ csrf_token }}",
question_number: question_number
},
type: "POST",
dataType: "json",
})
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) {
console.log(data);
let event = new CustomEvent('loadDicomViewerUrls', {
"detail": {"images" : data.images, "annotations" : data.annotations}
});
window.dispatchEvent(event);
$(".question-display-block .answers").empty().append(data.answers.toString());
n = parseInt(question_number) + 1
$(".question-display-block .question-number").empty().append(n);
// show some message according to the response.
// For eg. A message box showing that the status has been changed
})
.always(function () {
console.log('[Done]');
})
})
$(".view-question-link").first().click();
});
</script>
{% endblock %}
+105 -38
View File
@@ -7,29 +7,82 @@ app_name = "anatomy"
urlpatterns = [ urlpatterns = [
# path('', views.question_list, name='question_list'), # path('', views.question_list, name='question_list'),
path("", views.AnatomyExamViews.index, name="index"), path("", views.AnatomyExamViews.index, name="index"),
path("question/", cache_page(60 * 1)(views.AnatomyQuestionView.as_view()), name="anatomy_question_view"), path(
"question/",
cache_page(60 * 1)(views.AnatomyQuestionView.as_view()),
name="anatomy_question_view",
),
path("question/<int:pk>/", views.question_detail, name="question_detail"), path("question/<int:pk>/", views.question_detail, name="question_detail"),
path("question/create/", views.AnatomyQuestionCreate.as_view(), name="anatomy_question_create"), path(
path("question/<int:pk>/update", views.AnatomyQuestionUpdate.as_view(), name="anatomy_question_update"), "question/create/",
path("question/<int:pk>/save_annotation", views.question_save_annotation, name="question_save_annotation"), views.AnatomyQuestionCreate.as_view(),
path("question/<int:pk>/answer/", name="anatomy_question_create",
views.answer_question, ),
name="answer_question"), path(
path("question/<int:pk>/clone", views.QuestionClone.as_view(), name="question_clone"), "question/<int:pk>/update",
path("question/<int:pk>/delete", views.QuestionDelete.as_view(), name="question_delete"), views.AnatomyQuestionUpdate.as_view(),
name="anatomy_question_update",
),
path(
"question/<int:pk>/save_annotation",
views.question_save_annotation,
name="question_save_annotation",
),
path("question/<int:pk>/answer/", views.answer_question, name="answer_question"),
path(
"question/<int:pk>/clone", views.QuestionClone.as_view(), name="question_clone"
),
path(
"question/<int:pk>/delete",
views.QuestionDelete.as_view(),
name="question_delete",
),
path(
"exam/<int:pk>/review",
views.question_review,
name="question_review",
),
path("exam/<int:pk>/<int:sk>/mark", views.mark, name="mark"), path("exam/<int:pk>/<int:sk>/mark", views.mark, name="mark"),
path("exam/<int:pk>/mark", views.AnatomyExamViews.mark_overview, name="mark_overview"), path(
"exam/<int:pk>/mark", views.AnatomyExamViews.mark_overview, name="mark_overview"
),
path("exam/<int:pk>/<int:sk>/", views.exam_take, name="exam_take"), path("exam/<int:pk>/<int:sk>/", views.exam_take, name="exam_take"),
path("exam/<int:pk>/question/<int:sk>/", views.AnatomyExamViews.exam_question_detail, name="exam_question_detail"), path(
"exam/<int:pk>/question/<int:sk>/",
views.AnatomyExamViews.exam_question_detail,
name="exam_question_detail",
),
path("exam/<int:pk>/", views.AnatomyExamViews.exam_overview, name="exam_overview"), path("exam/<int:pk>/", views.AnatomyExamViews.exam_overview, name="exam_overview"),
path("exam/<int:pk>/json_edit", views.AnatomyExamViews.exam_json_edit, name="exam_json_edit"), path(
path("exam/<int:pk>/scores", cache_page(60 * 1)(views.exam_scores_cid), "exam/<int:pk>/json_edit",
name="exam_scores_cid"), views.AnatomyExamViews.exam_json_edit,
path("exam/<int:pk>/scores/<int:sk>/", views.exam_scores_cid_user, name="exam_json_edit",
name="exam_scores_cid_user"), ),
path("exam/<int:pk>/toggle_active", views.AnatomyExamViews.exam_toggle_active, name="exam_toggle_active"), path(
path("exam/<int:pk>/toggle_results_published", views.AnatomyExamViews.exam_toggle_results_published, name="exam_toggle_results_published"), "exam/<int:pk>/scores",
path("exam/submit", views.AnatomyExamViews.postExamAnswers, name="exam_answers_submit"), cache_page(60 * 1)(views.exam_scores_cid),
name="exam_scores_cid",
),
path(
"exam/<int:pk>/scores/<int:sk>/",
views.exam_scores_cid_user,
name="exam_scores_cid_user",
),
path(
"exam/<int:pk>/toggle_active",
views.AnatomyExamViews.exam_toggle_active,
name="exam_toggle_active",
),
path(
"exam/<int:pk>/toggle_results_published",
views.AnatomyExamViews.exam_toggle_results_published,
name="exam_toggle_results_published",
),
path(
"exam/submit",
views.AnatomyExamViews.postExamAnswers,
name="exam_answers_submit",
),
path("exam/", views.AnatomyExamViews.exam_list, name="exam_list"), path("exam/", views.AnatomyExamViews.exam_list, name="exam_list"),
path("exam/all", views.AnatomyExamViews.exam_list_all, name="exam_list_all"), path("exam/all", views.AnatomyExamViews.exam_list_all, name="exam_list_all"),
path("exam/create", views.ExamCreate.as_view(), name="exam_create"), path("exam/create", views.ExamCreate.as_view(), name="exam_create"),
@@ -37,25 +90,39 @@ urlpatterns = [
path("exam/<int:pk>/delete", views.ExamDelete.as_view(), name="exam_delete"), path("exam/<int:pk>/delete", views.ExamDelete.as_view(), name="exam_delete"),
path("exam/json/", views.AnatomyExamViews.active_exams, name="active_exams"), path("exam/json/", views.AnatomyExamViews.active_exams, name="active_exams"),
path("exam/json/<int:pk>", views.AnatomyExamViews.exam_json, name="exam_json"), path("exam/json/<int:pk>", views.AnatomyExamViews.exam_json, name="exam_json"),
path("exam/json/<int:pk>/recreate", views.AnatomyExamViews.exam_json_recreate, name="exam_json_recreate"), path(
#path("cid/", views.cid_selector, name="cid_selector"), "exam/json/<int:pk>/recreate",
views.AnatomyExamViews.exam_json_recreate,
name="exam_json_recreate",
),
# path("cid/", views.cid_selector, name="cid_selector"),
path("ajax/exam/flag/", views.flag_question, name="flag_question"), path("ajax/exam/flag/", views.flag_question, name="flag_question"),
path("body_part/create/", views.create_body_part, name="create_body_part"), path("body_part/create/", views.create_body_part, name="create_body_part"),
path("body_part/ajax/get_body_part_id", path(
views.get_body_part_id, "body_part/ajax/get_body_part_id",
name="get_body_part_id"), views.get_body_part_id,
path("structure/create/", name="get_body_part_id",
views.create_structure, ),
name="create_structure"), path("structure/create/", views.create_structure, name="create_structure"),
path("structure/ajax/get_structure_id", path(
views.get_structure_id, "structure/ajax/get_structure_id",
name="get_structure_id"), views.get_structure_id,
path("examination/create/", name="get_structure_id",
views.create_examination, ),
name="create_examination"), path("examination/create/", views.create_examination, name="create_examination"),
path("examination/ajax/get_examination_id", path(
views.get_examination_id, "examination/ajax/get_examination_id",
name="get_examination_id"), views.get_examination_id,
path("user_answers/", views.AnatomyUserAnswerView.as_view(), name="anatomy_user_answer_view"), name="get_examination_id",
path("user_answers/<int:pk>/delete", views.UserAnswerDelete.as_view(), name="user_answer_delete"), ),
path(
"user_answers/",
views.AnatomyUserAnswerView.as_view(),
name="anatomy_user_answer_view",
),
path(
"user_answers/<int:pk>/delete",
views.UserAnswerDelete.as_view(),
name="user_answer_delete",
),
] ]
+20 -1
View File
@@ -1031,4 +1031,23 @@ class ExamViewSet(RevisionMixin, viewsets.ModelViewSet):
class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView): class ExamDelete(RevisionMixin, AuthorOrCheckerRequiredMixin, DeleteView):
model = Exam model = Exam
template_name = "exam_confirm_delete.html" template_name = "exam_confirm_delete.html"
success_url = reverse_lazy("anatomy:index") success_url = reverse_lazy("anatomy:index")
def question_review(request, pk):
"""
Return a json representation of the question when the exam is active
"""
if request.is_ajax():
exam = get_object_or_404(Exam, pk=pk)
print(request.POST["question_number"])
print(type(request.POST["question_number"]))
if not exam.publish_results:
raise Http404
question = exam.exam_questions.all()[int(request.POST["question_number"])]
question_json = question.get_question_json(based=False)
return JsonResponse(question_json)
return JsonResponse({"status": "error"})
+36 -36
View File
@@ -1,40 +1,40 @@
{% extends 'rapids/base.html' %} {% extends 'rapids/base.html' %}
{% block content %} {% block content %}
<div class="question-display-block"> <div class="question-display-block">
Question <span class="question-number">1</span> Question <span class="question-number">1</span>
<div id="single-dicom-viewer" class="dicom-viewer" data-images="" data-annotations=''> <div id="single-dicom-viewer" class="dicom-viewer" data-images="" data-annotations=''>
</div>
<div class="answers">Answers: </div>
</div> </div>
<div class="answers">Answers: </div> <div class="rapids">
</div> <h2>Exam: {{ exam.name }}</h2>
<div class="rapids"> <h3>Candidate: {{ cid }}</h3>
<h2>Exam: {{ exam.name }}</h2> Answers:
<h3>Candidate: {{ cid }}</h3> <ul class="score-answer-list">{% for ans, score, correct_answer in answers_and_marks %}
Answers: <li class="user-answer-li">Question {{forloop.counter}} - Correct answer: <span
<ul class="score-answer-list">{% for ans, score, correct_answer in answers_and_marks %}
<li class="user-answer-li">Question {{forloop.counter}} - Correct answer: <span
class="correct-answer">{{ correct_answer }}</span></li> class="correct-answer">{{ correct_answer }}</span></li>
<span class="user-answer-score user-answer-score-{{score}} rapid-ans"> <span class="user-answer-score user-answer-score-{{score}} rapid-ans">
<pre>{{ans}}</pre> ({{score}}) <pre>{{ans}}</pre> ({{score}})
</span> </span>
<span class="view-question-link" data-qn={{forloop.counter0}}>View</span> <span class="view-question-link" data-qn={{forloop.counter0}}>View</span>
{% endfor %} {% endfor %}
</ul> </ul>
<br /> Total mark: {{ total_score }} / {{max_score}} <br /> Total mark: {{ total_score }} / {{max_score}}
<br /> Normalised score: {{ normalised_score }} <br /> Normalised score: {{ normalised_score }}
<div> <div>
<a href="{% url 'cid_scores' cid %}">Other exams</a> <a href="{% url 'cid_scores' cid %}">Other exams</a>
</div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$(".view-question-link").on("click", function (e) { $(".view-question-link").on("click", function (e) {
console.log("click", e) console.log("click", e)
question_number = e.currentTarget.dataset.qn; question_number = e.currentTarget.dataset.qn;
$("#single-dicom-viewer").empty(); $("#single-dicom-viewer").empty();
$.ajax({ $.ajax({
url: `{% url 'rapids:question_review' pk=exam.pk %}`, url: `{% url 'rapids:question_review' pk=exam.pk %}`,
data: { data: {
csrfmiddlewaretoken: "{{ csrf_token }}", csrfmiddlewaretoken: "{{ csrf_token }}",
@@ -44,7 +44,7 @@
dataType: "json", dataType: "json",
}) })
// $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly // $.ajax().done(), $.ajax().fail(), $ajax().always() are upto you. Add/change accordingly
.done(function (data) { .done(function (data) {
console.log(data); console.log(data);
let event = new CustomEvent('loadDicomViewerUrls', { let event = new CustomEvent('loadDicomViewerUrls', {
@@ -55,16 +55,16 @@
$(".question-display-block .answers").empty().append(data.answers.toString()); $(".question-display-block .answers").empty().append(data.answers.toString());
n = parseInt(question_number) + 1 n = parseInt(question_number) + 1
$(".question-display-block .question-number").empty().append(n); $(".question-display-block .question-number").empty().append(n);
// show some message according to the response. // show some message according to the response.
// For eg. A message box showing that the status has been changed // For eg. A message box showing that the status has been changed
}) })
.always(function () { .always(function () {
console.log('[Done]'); console.log('[Done]');
}) })
}) })
$(".view-question-link").first().click(); $(".view-question-link").first().click();
}); });
</script> </script>
{% endblock %} {% endblock %}
+10 -2
View File
@@ -17,7 +17,11 @@ urlpatterns = [
path( path(
"question/<int:pk>/", views.question_detail, name="question_detail" "question/<int:pk>/", views.question_detail, name="question_detail"
), # path("question/<int:pk>/json", views.question_json, name="question_json"), ), # path("question/<int:pk>/json", views.question_json, name="question_json"),
path("question/<int:pk>/save_annotation", views.question_save_annotation, name="question_save_annotation"), path(
"question/<int:pk>/save_annotation",
views.question_save_annotation,
name="question_save_annotation",
),
path("question/<int:pk>/split", views.rapid_split, name="rapid_split"), path("question/<int:pk>/split", views.rapid_split, name="rapid_split"),
path("question/<int:pk>/clone", views.RapidClone.as_view(), name="rapid_clone"), path("question/<int:pk>/clone", views.RapidClone.as_view(), name="rapid_clone"),
# path("verified/", views.verified, name="verified"), # path("verified/", views.verified, name="verified"),
@@ -54,7 +58,11 @@ urlpatterns = [
views.RapidExamViews.exam_json_edit, views.RapidExamViews.exam_json_edit,
name="exam_json_edit", name="exam_json_edit",
), ),
path("exam/<int:pk>/scores", cache_page(60 * 1)(views.exam_scores_cid), name="exam_scores_cid"), path(
"exam/<int:pk>/scores",
cache_page(60 * 1)(views.exam_scores_cid),
name="exam_scores_cid",
),
path( path(
"exam/<int:pk>/scores/<int:cid>/", "exam/<int:pk>/scores/<int:cid>/",
views.exam_scores_cid_user, views.exam_scores_cid_user,