Lots of improvemnets

This commit is contained in:
Ross
2024-01-15 22:46:41 +00:00
parent aa41423b22
commit 5c2578a3ce
22 changed files with 527 additions and 216 deletions
+7 -1
View File
@@ -150,6 +150,7 @@ class AnatomyQuestionForm(ModelForm):
"region", "region",
"modality", "modality",
"structure", "structure",
"feedback",
"examination", "examination",
"body_part", "body_part",
"description", "description",
@@ -158,9 +159,14 @@ class AnatomyQuestionForm(ModelForm):
widgets = { widgets = {
"structure": autocomplete.ModelSelect2(url="atlas:structure-autocomplete"), "structure": autocomplete.ModelSelect2(url="atlas:structure-autocomplete"),
"answer_help": TinyMCE(attrs={"cols": 80, "rows": 10}), "answer_help": TinyMCE(attrs={"cols": 80, "rows": 5}),
"feedback": TinyMCE(attrs={"cols": 80, "rows": 5}),
} }
class AnatomyQuestionAnswerForm(ModelForm):
class Meta:
model = AnatomyQuestion
fields = []
AnswerFormSet = inlineformset_factory( AnswerFormSet = inlineformset_factory(
AnatomyQuestion, AnatomyQuestion,
+12
View File
@@ -451,6 +451,18 @@ class Exam(ExamBase):
return exam_json return exam_json
def get_question_cid_user_answer(self, question_index, cid):
question = self.get_question_by_index(question_index)
answer = UserAnswer.objects.filter(question=question, cid=cid, exam=self)
return answer
def get_question_user_user_answer(self, question_index, user):
print(user)
question = self.get_question_by_index(question_index)
answer = UserAnswer.objects.filter(question=question, user=user.pk, exam=self)
return answer
@reversion.register @reversion.register
class UserAnswer(UserAnswerBase): class UserAnswer(UserAnswerBase):
@@ -26,17 +26,10 @@
document.addEventListener('drop', function (e) { e.preventDefault(); }, false); document.addEventListener('drop', function (e) { e.preventDefault(); }, false);
function add_input_form() {
var form_idx = $('#id_answers-TOTAL_FORMS').val();
$('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
$('#id_answers-TOTAL_FORMS').val(parseInt(form_idx) + 1);
}
$(document).ready(function () { $(document).ready(function () {
$("#add_examination").appendTo($("label[for='id_examination']")); $("#add_examination").appendTo($("label[for='id_examination']"));
$("#add_body_part").appendTo($("label[for='id_body_part']")); $("#add_body_part").appendTo($("label[for='id_body_part']"));
$("#add_structure").appendTo($("label[for='id_structure']"));
dropContainer = document.getElementById("drop-container"); dropContainer = document.getElementById("drop-container");
@@ -221,15 +214,18 @@
{{ form.media }} {{ form.media }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h2>Submit Question</h2>
{% if object %}
{% include 'anatomy/question_link_header.html' %}
{% endif %}
<h2>{{object|yesno:"Update,Create"}} Question</h2>
<form action="" method="post" enctype="multipart/form-data" id="anatomyquestion-form"> <form action="" method="post" enctype="multipart/form-data" id="anatomyquestion-form">
{% csrf_token %} {% csrf_token %}
<a href="/anatomy/examination/create" id="add_examination" class="add-popup" <a href="/anatomy/examination/create" id="add_examination" class="add-popup"
onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a> onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a>
<a href="/anatomy/body_part/create" id="add_body_part" class="add-popup" <a href="/anatomy/body_part/create" id="add_body_part" class="add-popup"
onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a> onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a>
<a href="/anatomy/structure/create" id="add_structure" class="add-popup" onclick="return showAddPopup(this);"><img
src="{% static '/img/icon-addlink.svg' %}"></a>
<table> <table>
{{ form.as_table }} {{ form.as_table }}
@@ -237,24 +233,6 @@
<div id="drop-container" class="drop-target">Drop image here <div id="drop-container" class="drop-target">Drop image here
<div id="drop-filenames"></div> <div id="drop-filenames"></div>
</div> </div>
<h3>Answers:</h3>
<input type="button" value="Add More Answers" id="add_more">
<div id="form_set">
{% for form in answer_formset %}
<ul class="no-error answer-formset">
{{form.non_field_errors}}
{{form.errors}}
{{ form.as_ul }}
</ul>
{% endfor %}
</div>
{{ answer_formset.management_form }}
<input type="submit" class="submit-button" value="Submit" name="submit"> <input type="submit" class="submit-button" value="Submit" name="submit">
<input type="submit" class="submit-button" value="Submit and Clone" name="submit-clone">
</form> </form>
<div id="empty_form" style="display:none">
<ul class='no_error answer-formset'>
{{ answer_formset.empty_form.as_ul }}
</ul>
</div>
{% endblock %} {% endblock %}
@@ -0,0 +1,56 @@
{% extends "anatomy/base.html" %}
{% load static %}
{% block js %}
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
<script type="text/javascript">
function add_input_form() {
var form_idx = $('#id_answers-TOTAL_FORMS').val();
$('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
$('#id_answers-TOTAL_FORMS').val(parseInt(form_idx) + 1);
}
$(document).ready(function () {
$('#add_more').click(() => {
add_input_form()
});
});
</script>
{{ form.media }}
{% endblock %}
{% block content %}
{% if object %}
{% include 'anatomy/question_link_header.html' %}
{% endif %}
<h2>Question Answers</h2>
This page can be used to add and modify answers associated with a question. Answers in here will be used for automarking submitted answers so it is best not to remove. Updating an answer here will not automatically update a submitted answers (scores cache would need to be refreshed)
<form action="" method="post" enctype="multipart/form-data" id="anatomyquestion-form">
{% csrf_token %}
<h3>Answers:</h3>
<div id="form_set">
{% for form in answer_formset %}
<ul class="no-error answer-formset">
{{form.non_field_errors}}
{{form.errors}}
{{ form.as_ul }}
</ul>
{% endfor %}
</div>
{{ answer_formset.management_form }}
<input type="button" value="Add More Answers" id="add_more">
<p>
<input type="submit" class="submit-button" value="Submit" name="submit">
</p>
</form>
<div id="empty_form" style="display:none">
<ul class='no_error answer-formset'>
{{ answer_formset.empty_form.as_ul }}
</ul>
</div>
{% endblock %}
+15 -10
View File
@@ -1,19 +1,24 @@
{% extends 'generic/exam_scores_base.html' %} {% extends 'generic/exam_scores_base.html' %}
{% block table_answers %} {% block table_answers %}
{% for question in questions %} {% for question in questions %}
<tr> <tr>
<td><a href="{% url 'anatomy:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter}}</a> <td><a href="{% url 'anatomy:mark' exam_pk=exam.pk sk=forloop.counter0 %}">Question {{forloop.counter}}</a>
</td> </td>
{% comment %} {% for cid in cids %} {% 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> <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 %} {% endfor %} {% endcomment %}
{% for cid in cids %} {% for cid in cids %}
{% with by_question|get_item:question|get_item:cid as ans_score %} {% with by_question|get_item:question|get_item:cid as ans_score %}
<td class="anatomy-ans user-answer-score-{{ans_score.1}}" title="answer score: {{ans_score.1}}">{{ans_score.0}}</td> <td class="anatomy-ans user-answer-score-{{ans_score.1}}"
{% endwith %} title="answer score: {{ans_score.1}}"
{% endfor %} data-examquestionindex={{forloop.parentloop.counter0}}
</tr> data-cid={{cid}}
>{{ans_score.0}}<a class="show-on-hover" href="{% url 'anatomy:exam_question_user_answer' exam.pk forloop.parentloop.counter0 cid|slice:':1' cid|slice:'2:' %}"><i class="bi bi-pen"></i>
</a></td>
{% endwith %}
{% endfor %} {% endfor %}
</tr>
{% endfor %}
{% endblock table_answers %} {% endblock table_answers %}
+87 -112
View File
@@ -1,45 +1,20 @@
{% extends 'anatomy/base.html' %} {% extends 'anatomy/base.html' %}
{% block content %} {% block content %}
{% load static %} {% load static %}
<!-- testing --> <!-- testing -->
<!--<div id="dicom-image" data-url="http://localhost:8000/static/abdoct.jpg" <!--<div id="dicom-image" data-url="http://localhost:8000/static/abdoct.jpg"
data-annotations='{{question.image_annotations}}' data-edit_annotation=true></div>--> data-annotations='{{question.image_annotations}}' data-edit_annotation=true></div>-->
<div class="floating-header"> {% include 'anatomy/question_link_header.html' %}
<a href="{% url 'anatomy:anatomy_question_update' question.id %}" title="Edit the Question">Edit</a> <button id="save-annotations">Save Annotations</button>
<a href="{% url 'anatomy:question_clone' question.id %}" title="Clone the Question">Clone</a> <div id="dicom-image" class="dicom-image" data-url="https://www.penracourses.org.uk{{ question.image.url}}"
<a href="{% url 'anatomy:question_delete' pk=question.pk %}" title="Delete the Question">Delete</a> data-annotations='{{question.image_annotations}}' data-edit_annotation=true></div>
<a href="#" <div class="question">
onclick="return window.create_popup_window('{% url 'feedback_create' question_type='anatomy' pk=question.pk %}')"> <div class="date">
Add Note</a> Created: {{ question.created_date|date:"d/m/Y" }}
</div>
{% if request.user.is_superuser %} <h1>{{ question.get_primary_answer }}</h1>
<a href="{% url 'admin:anatomy_anatomyquestion_change' question.id %}" <h2>{{question.question_type}}</h2>
title="Edit the Question using the admin interface">Admin Edit</a>
<a href="{% url 'anatomy:question_user_answers' question.id %}" title="View user answers associated with this question">User answers</a>
{% endif %}
{% if exam %}
<div>
{% if previous > -1 %}
<a href="{% url 'anatomy:exam_question_detail' exam.id previous %}">Previous question</a>
{% endif %}
Viewing question as part of exam: <a href="{% url 'anatomy:exam_overview' exam.id %}">{{exam.name}}</a> [{{pos}}/{{exam_length}}]
{% if next %}
<a href="{% url 'anatomy:exam_question_detail' exam.id next %}">Next question</a>
{% endif %}
</div>
{% endif %}
</div>
<button id="save-annotations">Save Annotations</button>
<div id="dicom-image" class="dicom-image" data-url="https://www.penracourses.org.uk{{ question.image.url}}"
data-annotations='{{question.image_annotations}}' data-edit_annotation=true></div>
<div class="question">
<div class="date">
Created: {{ question.created_date|date:"d/m/Y" }}
</div>
<h1>{{ question.get_primary_answer }}</h1>
<h2>{{question.question_type}}</h2>
<details> <details>
<summary> <summary>
Answers: Answers:
@@ -50,7 +25,7 @@
<tr> <tr>
<td> <td>
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy" <span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy"
{% endif %}> {% endif %}>
{{ answer }} {{ answer }}
</span> </span>
<td> <td>
@@ -59,67 +34,67 @@
{% endfor %} {% endfor %}
</table> </table>
</details> </details>
<div> <div>
Answer help: {{ question.answer_help|safe }} Answer help: {{ question.answer_help|safe }}
</div> </div>
<div> <div>
Answer suggest incorrect: {{ question.answer_suggest_incorrect }} Answer suggest incorrect: {{ question.answer_suggest_incorrect }}
</div> </div>
<div> <div>
Description: {{ question.description }} Description: {{ question.description }}
</div> </div>
<div> <div>
Exams: {% for exam in question.exams.all %} Exams: {% for exam in question.exams.all %}
<a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">{{ exam.name }}</a> <a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">{{ exam.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
<div> <div>
Modality: {{ question.modality }} Modality: {{ question.modality }}
</div> </div>
<div> <div>
Region: {{ question.region }} Region: {{ question.region }}
</div> </div>
<div> <div>
Structure: {{ question.structure }} Structure: {{ question.structure }}
</div> </div>
<div> <div>
Body Part: {{ question.body_part }} Body Part: {{ question.body_part }}
</div> </div>
<div> <div>
Author: {% for author in question.author.all %} Author: {% for author in question.author.all %}
{{ author }}, {{ author }},
{% endfor %} {% endfor %}
</div> </div>
<div> <div>
Open access: {{ question.open_access }} Open access: {{ question.open_access }}
</div> </div>
{% include 'question_notes.html' %} {% include 'question_notes.html' %}
<div> <div>
<details> <details>
<summary> <summary>
<span id="annotation-json-toggle">Annotation JSON (+/-):</span> <span id="annotation-json-content"> <span id="annotation-json-toggle">Annotation JSON (+/-):</span> <span id="annotation-json-content">
</summary> </summary>
{% if question.image_annotations %} {% if question.image_annotations %}
<pre>{{ question.image_annotations }}</pre> <pre>{{ question.image_annotations }}</pre>
{% else %} {% else %}
No saved annotations. No saved annotations.
{% endif %} {% endif %}
</span> </span>
</details> </details>
</div>
</div> </div>
</div> <script type="text/javascript">
<script type="text/javascript"> $(document).ready(function () {
$(document).ready(function () {
//$("#annotation-json-toggle").click(() => { //$("#annotation-json-toggle").click(() => {
// $("#annotation-json-content").toggle() // $("#annotation-json-content").toggle()
//}); //});
// send request to change the is_private state on customSwitches toggle // send request to change the is_private state on customSwitches toggle
$("#save-annotations").click(function () { $("#save-annotations").click(function () {
json = getJsonToolStateNoId(); json = getJsonToolStateNoId();
$.ajax({ $.ajax({
url: "{% url 'anatomy:question_save_annotation' pk=question.pk %}", url: "{% url 'anatomy:question_save_annotation' pk=question.pk %}",
data: { data: {
csrfmiddlewaretoken: "{{ csrf_token }}", csrfmiddlewaretoken: "{{ csrf_token }}",
@@ -134,38 +109,38 @@
}) })
// $.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);
// 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
if (data.status == "success") { if (data.status == "success") {
toastr.info('Annotations saved') toastr.info('Annotations saved')
} else { } else {
toastr.warning('Error saving annotations') toastr.warning('Error saving annotations')
} }
}) })
.always(function () { .always(function () {
console.log('[Done]'); console.log('[Done]');
}) })
}) })
}); });
function getJsonToolStateNoId() { function getJsonToolStateNoId() {
const el = document.getElementById("dicom-image"); const el = document.getElementById("dicom-image");
const c = cornerstone.getEnabledElement(el); const c = cornerstone.getEnabledElement(el);
const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager; const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
const image_id = c.image.imageId; const image_id = c.image.imageId;
const state_with_id = toolStateManager.saveToolState() const state_with_id = toolStateManager.saveToolState()
const json_tool_state = JSON.stringify(state_with_id[image_id]); const json_tool_state = JSON.stringify(state_with_id[image_id]);
return json_tool_state; return json_tool_state;
} }
</script> </script>
{% endblock %} {% endblock %}
@@ -0,0 +1,28 @@
<div class="floating-header">
<a href="{% url 'anatomy:anatomy_question_update' question.id %}" title="Edit the Question">Edit</a>
<a href="{% url 'anatomy:question_clone' question.id %}" title="Clone the Question">Clone</a>
<a href="{% url 'anatomy:question_delete' pk=question.pk %}" title="Delete the Question">Delete</a>
<a href="{% url 'anatomy:question_answer_update' pk=question.pk %}" title="Update the question answers">Edit Answers</a>
<a href="#"
onclick="return window.create_popup_window('{% url 'feedback_create' question_type='anatomy' pk=question.pk %}')">
Add Note</a>
{% if request.user.is_superuser %}
<a href="{% url 'admin:anatomy_anatomyquestion_change' question.id %}"
title="Edit the Question using the admin interface">Admin Edit</a>
<a href="{% url 'anatomy:question_user_answers' question.id %}" title="View user answers associated with this question">User answers</a>
{% endif %}
{% if exam %}
<div>
{% if previous > -1 %}
<a href="{% url 'anatomy:exam_question_detail' exam.id previous %}">Previous question</a>
{% endif %}
Viewing question as part of exam: <a href="{% url 'anatomy:exam_overview' exam.id %}">{{exam.name}}</a> [{{pos}}/{{exam_length}}]
{% if next %}
<a href="{% url 'anatomy:exam_question_detail' exam.id next %}">Next question</a>
{% endif %}
</div>
{% endif %}
</div>
@@ -1,6 +1,7 @@
{% extends 'anatomy/base.html' %} {% extends 'anatomy/base.html' %}
{% block content %} {% block content %}
{% include 'anatomy/question_link_header.html' %}
<h2>{{question}}</h2> <h2>{{question}}</h2>
{% if answer_compare %} {% if answer_compare %}
+5
View File
@@ -28,6 +28,11 @@ urlpatterns = [
views.AnatomyQuestionUpdate.as_view(), views.AnatomyQuestionUpdate.as_view(),
name="anatomy_question_update", name="anatomy_question_update",
), ),
path(
"question/<int:pk>/update_answers",
views.AnatomyQuestionAnswerUpdate.as_view(),
name="question_answer_update",
),
path( path(
"question/<int:pk>/save_annotation", "question/<int:pk>/save_annotation",
views.question_save_annotation, views.question_save_annotation,
+65 -23
View File
@@ -28,6 +28,7 @@ from dal import autocomplete
from .forms import ( from .forms import (
AnatomyAnswerForm, AnatomyAnswerForm,
AnatomyQuestionAnswerForm,
AnswerFormSet, AnswerFormSet,
AnswerUpdateFormSet, AnswerUpdateFormSet,
BodyPartForm, BodyPartForm,
@@ -247,7 +248,7 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
elif not unmarked_exam_answers_only: elif not unmarked_exam_answers_only:
mark_url = "anatomy:mark_all" mark_url = "anatomy:mark_all"
questions = exam.exam_questions.all() questions = exam.exam_questions.all().prefetch_related("answers")
n = sk n = sk
@@ -358,6 +359,7 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
for suggest_incorrect in question.answer_suggest_incorrect: for suggest_incorrect in question.answer_suggest_incorrect:
if suggest_incorrect in ans: if suggest_incorrect in ans:
answer_suggest_incorrect.append(ans) answer_suggest_incorrect.append(ans)
break
return render( return render(
request, request,
@@ -554,6 +556,42 @@ class AnatomyQuestionCreate(AnatomyQuestionCreateBase):
# return super().form_valid(form) # return super().form_valid(form)
class AnatomyQuestionAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
model = AnatomyQuestion
form_class = AnatomyQuestionAnswerForm
template_name = "anatomy/anatomyquestionanswer_form.html"
def get_context_data(self, **kwargs):
context = super(AnatomyQuestionAnswerUpdate, self).get_context_data(**kwargs)
if self.request.POST:
context["answer_formset"] = AnswerUpdateFormSet(
self.request.POST, self.request.FILES, instance=self.object
)
context["answer_formset"].full_clean()
else:
context["answer_formset"] = AnswerUpdateFormSet(instance=self.object)
context["question"] = context["object"]
return context
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.save()
#form.instance.author.add(self.request.user.id)
context = self.get_context_data(form=form)
formset = context["answer_formset"]
# logger.debug(formset.is_valid())
if formset.is_valid():
response = super().form_valid(form)
formset.instance = self.object
formset.save()
return response
else:
return super().form_invalid(form)
# class AnatomyQuestionUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, # class AnatomyQuestionUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin,
class AnatomyQuestionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView): class AnatomyQuestionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
model = AnatomyQuestion model = AnatomyQuestion
@@ -566,13 +604,14 @@ class AnatomyQuestionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateV
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AnatomyQuestionUpdate, self).get_context_data(**kwargs) context = super(AnatomyQuestionUpdate, self).get_context_data(**kwargs)
if self.request.POST: #if self.request.POST:
context["answer_formset"] = AnswerUpdateFormSet( # context["answer_formset"] = AnswerUpdateFormSet(
self.request.POST, self.request.FILES, instance=self.object # self.request.POST, self.request.FILES, instance=self.object
) # )
context["answer_formset"].full_clean() # context["answer_formset"].full_clean()
else: #else:
context["answer_formset"] = AnswerUpdateFormSet(instance=self.object) # context["answer_formset"] = AnswerUpdateFormSet(instance=self.object)
context["question"] = context["object"]
return context return context
def form_valid(self, form): def form_valid(self, form):
@@ -589,23 +628,24 @@ class AnatomyQuestionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateV
form.instance.author.add(self.request.user.id) form.instance.author.add(self.request.user.id)
context = self.get_context_data(form=form) context = self.get_context_data(form=form)
formset = context["answer_formset"] #formset = context["answer_formset"]
# logger.debug(formset.is_valid()) return super().form_valid(form)
if formset.is_valid(): ## logger.debug(formset.is_valid())
response = super().form_valid(form) #if formset.is_valid():
formset.instance = self.object # response = super().form_valid(form)
formset.save() # formset.instance = self.object
# formset.save()
# restore exam orders # # restore exam orders
#for exam in self.object.exams.all(): # #for exam in self.object.exams.all():
# if exam in exam_orders and self.object in exam_orders[exam]: # # if exam in exam_orders and self.object in exam_orders[exam]:
# print(exam_orders[exam]) # # print(exam_orders[exam])
# exam.exam_questions.set(exam_orders[exam]) # # exam.exam_questions.set(exam_orders[exam])
# exam.save() # # exam.save()
return response # return response
else: #else:
return super().form_invalid(form) # return super().form_invalid(form)
class QuestionClone(AnatomyQuestionCreateBase): class QuestionClone(AnatomyQuestionCreateBase):
@@ -625,6 +665,8 @@ class QuestionClone(AnatomyQuestionCreateBase):
# initial_data["exams"] = list(exams) # initial_data["exams"] = list(exams)
# TODO: clone answers?
return initial_data return initial_data
+16 -1
View File
@@ -181,6 +181,11 @@ class QuestionBase(models.Model):
"""If this makes sense in the question/answer context override""" """If this makes sense in the question/answer context override"""
return None return None
def get_author_objects(self):
"""Returns a comma seperated text list of authors"""
authors = [i for i in self.author.all()]
return authors
class SeriesImageBase(models.Model): class SeriesImageBase(models.Model):
""" """
@@ -320,7 +325,7 @@ class SeriesBase(models.Model):
return False return False
def get_author_objects(self): def get_author_objects(self):
"""Returns a comma seperated text list of authors""" """Returns a list of authors"""
if self.author: if self.author:
return self.author.all() return self.author.all()
else: else:
@@ -703,6 +708,12 @@ class ExamCollectionGenericBase(models.Model):
new.save() new.save()
return new return new
def get_question_cid_user_answer(self, question_index, cid):
raise NotImplementedError
def get_question_user_user_answer(self, question_index, user):
raise NotImplementedError
def get_cid_user_exams( def get_cid_user_exams(
self, cid_user: "CidUser" = None, user_user: User = None self, cid_user: "CidUser" = None, user_user: User = None
) -> "CidUserExam": ) -> "CidUserExam":
@@ -833,6 +844,10 @@ class ExamBase(ExamCollectionGenericBase):
def get_question_index(self, question): def get_question_index(self, question):
return list(self.exam_questions.all()).index(question) return list(self.exam_questions.all()).index(question)
def get_question_by_index(self, index:int):
# There must be a better way to do this
return list(self.exam_questions.all())[index]
def get_cid_user_score(self, cid_user): def get_cid_user_score(self, cid_user):
c = "c/" + str(cid_user) c = "c/" + str(cid_user)
if c in self.user_scores: if c in self.user_scores:
+41 -13
View File
@@ -11,9 +11,19 @@ urlpatterns = [
# path('', views.question_list, name='question_list'), # path('', views.question_list, name='question_list'),
path("examination/", views.ExaminationView.as_view(), name="examination_view"), path("examination/", views.ExaminationView.as_view(), name="examination_view"),
path("examination/<int:pk>", views.examination_detail, name="examination_detail"), path("examination/<int:pk>", views.examination_detail, name="examination_detail"),
path("examination/<int:pk>/delete", views.ExaminationDelete.as_view(), name="examination_delete"), path(
path("examination/<int:pk>/update", views.ExaminationUpdate.as_view(), name="examination_update"), "examination/<int:pk>/delete",
path("examination/<int:pk>/merge", views.examination_merge, name="examination_merge"), views.ExaminationDelete.as_view(),
name="examination_delete",
),
path(
"examination/<int:pk>/update",
views.ExaminationUpdate.as_view(),
name="examination_update",
),
path(
"examination/<int:pk>/merge", views.examination_merge, name="examination_merge"
),
path("examination/create/", views.create_examination, name="create_examination"), path("examination/create/", views.create_examination, name="create_examination"),
path( path(
"examination/ajax/get_examination_id", "examination/ajax/get_examination_id",
@@ -60,7 +70,11 @@ urlpatterns = [
name="cid_group_detail", name="cid_group_detail",
), ),
path("cids/group/<int:pk>/email", views.group_email, name="group_email"), path("cids/group/<int:pk>/email", views.group_email, name="group_email"),
path("cids/group/<int:pk>/exams", views.CidGroupExamUpdate.as_view(), name="cid_group_exams"), path(
"cids/group/<int:pk>/exams",
views.CidGroupExamUpdate.as_view(),
name="cid_group_exams",
),
path( path(
"cids/group/<int:pk>/email_results", "cids/group/<int:pk>/email_results",
views.group_email_results, views.group_email_results,
@@ -106,7 +120,11 @@ urlpatterns = [
views.user_group_view_detail, views.user_group_view_detail,
name="user_group_detail", name="user_group_detail",
), ),
path("user/group/<int:pk>/exams", views.UserGroupExamUpdate.as_view(), name="user_group_exams"), path(
"user/group/<int:pk>/exams",
views.UserGroupExamUpdate.as_view(),
name="user_group_exams",
),
path( path(
"user/group/<int:pk>/update", "user/group/<int:pk>/update",
views.UserUserGroupUpdate.as_view(), views.UserUserGroupUpdate.as_view(),
@@ -191,6 +209,16 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
generic_exam_view.exam_question_detail, generic_exam_view.exam_question_detail,
name="exam_question_detail", name="exam_question_detail",
), ),
#path(
# "exam/<int:pk>/question/<int:sk>/answer/<str:user_or_cid>",
# generic_exam_view.exam_question_user_answer,
# name="exam_question_user_answer",
#),
path(
"exam/<int:pk>/question/<int:sk>/answer/<str:c_or_u>/<str:user_or_cid>",
generic_exam_view.exam_question_user_answer,
name="exam_question_user_answer",
),
path( path(
"exam/<int:exam_id>/user/<int:user_id>", "exam/<int:exam_id>/user/<int:user_id>",
generic_exam_view.exam_user, generic_exam_view.exam_user,
@@ -333,26 +361,26 @@ def generic_exam_urls(generic_exam_view: GenericExamViews):
generic_exam_view.exam_question_json_unbased_cid, generic_exam_view.exam_question_json_unbased_cid,
name="exam_question_json_unbased_cid", name="exam_question_json_unbased_cid",
), ),
#path( # path(
# "exam/json/<int:pk>/<int:sk>", # "exam/json/<int:pk>/<int:sk>",
# generic_exam_view.exam_question_json, # generic_exam_view.exam_question_json,
# name="exam_question_json", # name="exam_question_json",
#), # ),
#path( # path(
# "exam/json/<int:pk>/<int:sk>/unbased", # "exam/json/<int:pk>/<int:sk>/unbased",
# generic_exam_view.exam_question_json_unbased, # generic_exam_view.exam_question_json_unbased,
# name="exam_question_json_unbased", # name="exam_question_json_unbased",
#), # ),
#path( # path(
# "exam/json/<int:pk>/<int:cid>/<str:passcode>/<int:sk>", # "exam/json/<int:pk>/<int:cid>/<str:passcode>/<int:sk>",
# generic_exam_view.exam_question_json_cid, # generic_exam_view.exam_question_json_cid,
# name="exam_question_json_cid", # name="exam_question_json_cid",
#), # ),
#path( # path(
# "exam/json/<int:pk>/<int:cid>/<str:passcode>/<int:sk>/unbased", # "exam/json/<int:pk>/<int:cid>/<str:passcode>/<int:sk>/unbased",
# generic_exam_view.exam_question_json_unbased_cid, # generic_exam_view.exam_question_json_unbased_cid,
# name="exam_question_json_unbased_cid", # name="exam_question_json_unbased_cid",
#), # ),
path( path(
"exam/json/<int:pk>/recreate", "exam/json/<int:pk>/recreate",
generic_exam_view.exam_json_recreate, generic_exam_view.exam_json_recreate,
+29
View File
@@ -1353,6 +1353,35 @@ class ExamViews(View, LoginRequiredMixin):
{"filter": filter, "app_name": self.app_name}, {"filter": filter, "app_name": self.app_name},
) )
@method_decorator(login_required)
def exam_question_user_answer(self, request, pk: int, sk: int, c_or_u: str, user_or_cid: str):
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
#question = exam.exam_questions.all()[sk]
if c_or_u == "u":
user = get_object_or_404(User,pk=user_or_cid)
answer = exam.get_question_user_user_answer(sk, user)
elif c_or_u == "c":
#cid_user = CidUser.objects.filter(cid=user_or_cid)
answer =exam.get_question_cid_user_answer(sk, user_or_cid)
else:
raise Http404
print(answer)
return HttpResponse(answer)
@method_decorator(login_required)
def exam_question_cid_user_answer(self, request, pk: int, sk: int, cid: str):
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
#question = exam.exam_questions.all()[sk]
cid_user = CidUser.objects.filter(cid=cid)
answer =exam.get_question_cid_user_answer(sk, cid_user)
print(answer)
@method_decorator(login_required) @method_decorator(login_required)
def exam_question_detail(self, request, pk, sk): def exam_question_detail(self, request, pk, sk):
exam = get_object_or_404(self.Exam, pk=pk) exam = get_object_or_404(self.Exam, pk=pk)
+8
View File
@@ -1109,4 +1109,12 @@ table .peninsula-trainee::before {
.current-question { .current-question {
background-color: #52057b; background-color: #52057b;
color: grey; color: grey;
}
.show-on-hover {
opacity: 0%;
}
.show-on-hover:hover {
opacity: 100%;
} }
+5
View File
@@ -64,6 +64,11 @@ class RapidCreationDefaultForm(ModelForm):
exclude = ["author"] exclude = ["author"]
class RapidQuestionAnswerForm(ModelForm):
class Meta:
model = Rapid
fields = []
class RapidForm(ModelForm): class RapidForm(ModelForm):
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False)) # exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False))
+2 -25
View File
@@ -1,29 +1,6 @@
{% extends 'rapids/base.html' %} {% extends 'rapids/base.html' %}
{% block content %} {% block content %}
{% include "rapids/question_link_header.html" %}
<div class="floating-header"> {% include 'rapids/question_display_block.html' %}
<a href="{% url 'rapids:rapid_update' pk=question.pk %}" title="Edit the Rapid">Edit</a>
<a href="{% url 'rapids:rapid_clone' pk=question.pk %}" title="Clone the Rapid (duplicate everything but the images)">Clone</a>
<a href="{% url 'rapids:question_delete' pk=question.pk %}" title="Delete the Rapid">Delete</a>
<a href="#" onclick="return window.create_popup_window('{% url 'feedback_create' question_type='rapid' pk=question.pk %}')"> Add Note</a>
{% if request.user.is_superuser %}
<a href="{% url 'admin:rapids_rapid_change' question.id %}" title="Edit the Rapid using the admin interface">Admin Edit</a>
<a href="{% url 'rapids:question_user_answers' question.id %}" title="View user answers associated with this question">User answers</a>
{% endif %}
{% if exam %}
<div>
{% if previous > -1 %}
<a href="{% url 'rapids:exam_question_detail' exam.id previous %}">Previous question</a>
{% endif %}
Viewing question as part of exam: <a href="{% url 'rapids:exam_overview' exam.id %}">{{exam.name}}</a> [{{pos}}/{{exam_length}}]
{% if next %}
<a href="{% url 'rapids:exam_question_detail' exam.id next %}">Next question</a>
{% endif %}
</div>
{% endif %}
</div>
{% include 'rapids/question_display_block.html' %}
{% endblock %} {% endblock %}
@@ -0,0 +1,25 @@
<div class="floating-header">
<a href="{% url 'rapids:rapid_update' pk=question.pk %}" title="Edit the Rapid">Edit</a>
<a href="{% url 'rapids:rapid_clone' pk=question.pk %}" title="Clone the Rapid (duplicate everything but the images)">Clone</a>
<a href="{% url 'rapids:question_delete' pk=question.pk %}" title="Delete the Rapid">Delete</a>
<a href="{% url 'rapids:question_answer_update' pk=question.pk %}" title="Update the question answers">Edit Answers</a>
<a href="#" onclick="return window.create_popup_window('{% url 'feedback_create' question_type='rapid' pk=question.pk %}')"> Add Note</a>
{% if request.user.is_superuser %}
<a href="{% url 'admin:rapids_rapid_change' question.id %}" title="Edit the Rapid using the admin interface">Admin Edit</a>
<a href="{% url 'rapids:question_user_answers' question.id %}" title="View user answers associated with this question">User answers</a>
{% endif %}
{% if exam %}
<div>
{% if previous > -1 %}
<a href="{% url 'rapids:exam_question_detail' exam.id previous %}">Previous question</a>
{% endif %}
Viewing question as part of exam: <a href="{% url 'rapids:exam_overview' exam.id %}">{{exam.name}}</a> [{{pos}}/{{exam_length}}]
{% if next %}
<a href="{% url 'rapids:exam_question_detail' exam.id next %}">Next question</a>
{% endif %}
</div>
{% endif %}
</div>
+5 -1
View File
@@ -360,6 +360,11 @@
{{ form.media }} {{ form.media }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if object %}
{% include "rapids/question_link_header.html" %}
{% endif %}
<h2>Submit Rapid</h2> <h2>Submit Rapid</h2>
<a href="{% url 'rapids:rapid_create_defaults' %}">Edit defaults</a> <a href="{% url 'rapids:rapid_create_defaults' %}">Edit defaults</a>
<p> <p>
@@ -415,7 +420,6 @@ the feedback image box is checked they will not be displayed when taking the que
</div> </div>
{{ image_formset.management_form }} {{ image_formset.management_form }}
<input type="submit" class="submit-button" value="Submit" name="submit"> <input type="submit" class="submit-button" value="Submit" name="submit">
<input type="submit" class="submit-button" value="Submit and Clone" name="submit-clone">
</form> </form>
<div id="empty_form" style="display:none"> <div id="empty_form" style="display:none">
<ul class='no_error image-formset'> <ul class='no_error image-formset'>
+71
View File
@@ -0,0 +1,71 @@
{% extends "rapids/base.html" %}
{% load static from static %}
{% block js %}
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
<script type="text/javascript">
let csrf_token = "{{csrf_token}}";
function add_answers_input_form() {
var form_idx = $('#id_answers-TOTAL_FORMS').val();
$('#answer_form_set').append($('#answer_empty_form').html().replace(/__prefix__/g, form_idx));
$(`#id_answers-${form_idx}-status`).val(2);
$('#id_answers-TOTAL_FORMS').val(parseInt(form_idx) + 1);
}
$(document).ready(function () {
$('#add_more_answers').click(() => {
add_answers_input_form()
});
});
</script>
{{ form.media }}
{% endblock %}
{% block content %}
{% if object %}
{% include "rapids/question_link_header.html" %}
{% endif %}
<h2>Update Question Answers</h2>
<form action="" method="post" enctype="multipart/form-data" id="rapid-form">
{% csrf_token %}
<h3>Answers:</h3>
<div id="answer_form_set">
{% for form in answer_formset %}
<ul class="no-error answer-formset">
{{form.non_field_errors}}
{{form.errors}}
{{ form.as_ul }}
</ul>
{% endfor %}
</div>
{{ answer_formset.management_form }}
<input type="button" value="Add More Answers" id="add_more_answers">
<p>
<input type="submit" class="submit-button" value="Submit" name="submit">
</p>
</form>
<div id="answer_empty_form" style="display:none">
<ul class='no_error answer-formset'>
{{ answer_formset.empty_form.as_ul }}
</ul>
</div>
{% endblock %}
+5
View File
@@ -81,6 +81,11 @@ urlpatterns = [
views.RapidUpdate.as_view(), views.RapidUpdate.as_view(),
name="rapid_update", name="rapid_update",
), ),
path(
"question/<int:pk>/update_answers",
views.RapidAnswerUpdate.as_view(),
name="question_answer_update",
),
path( path(
"user_answers/", "user_answers/",
views.UserAnswerTableView.as_view(), views.UserAnswerTableView.as_view(),
+38 -1
View File
@@ -24,6 +24,7 @@ from .forms import (
RapidForm, RapidForm,
MarkRapidQuestionForm, MarkRapidQuestionForm,
ImageFormSet, ImageFormSet,
RapidQuestionAnswerForm,
RegionForm, RegionForm,
AbnormalityForm, AbnormalityForm,
ExaminationForm, ExaminationForm,
@@ -330,9 +331,44 @@ class RapidCreate(RapidCreateBase):
# form.instance.author.add(self.request.user.id) # form.instance.author.add(self.request.user.id)
# return super().form_valid(form) # return super().form_valid(form)
class RapidAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
model = Rapid
form_class = RapidQuestionAnswerForm
template_name = "rapids/rapidquestionanswer_form.html"
def get_context_data(self, **kwargs):
context = super(RapidAnswerUpdate, self).get_context_data(**kwargs)
if self.request.POST:
context["answer_formset"] = AnswerUpdateFormSet(
self.request.POST, self.request.FILES, instance=self.object
)
context["answer_formset"].full_clean()
else:
context["answer_formset"] = AnswerUpdateFormSet(instance=self.object)
context["question"] = context["object"]
return context
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.save()
#form.instance.author.add(self.request.user.id)
context = self.get_context_data(form=form)
formset = context["answer_formset"]
# logger.debug(formset.is_valid())
if formset.is_valid():
response = super().form_valid(form)
formset.instance = self.object
formset.save()
return response
else:
return super().form_invalid(form)
class RapidUpdate( class RapidUpdate(
RevisionMixin, LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView
): ):
model = Rapid model = Rapid
form_class = RapidForm form_class = RapidForm
@@ -360,6 +396,7 @@ class RapidUpdate(
else: else:
context["image_formset"] = ImageFormSet(instance=self.object) context["image_formset"] = ImageFormSet(instance=self.object)
context["answer_formset"] = AnswerFormSet(instance=self.object) context["answer_formset"] = AnswerFormSet(instance=self.object)
context["question"] = context["object"]
return context return context
def form_valid(self, form): def form_valid(self, form):
-1
View File
@@ -19,6 +19,5 @@
{{ form.as_table }} {{ form.as_table }}
</table> </table>
<input type="submit" class="submit-button" value="Submit" name="submit"> <input type="submit" class="submit-button" value="Submit" name="submit">
<input type="submit" class="submit-button" value="Submit and Clone" name="submit-clone">
</form> </form>
{% endblock %} {% endblock %}