improve suggest answers (and fix some other issues)
This commit is contained in:
+6
-1
@@ -252,4 +252,9 @@ class ExamMarkerForm(ExamMarkerFormMixin):
|
|||||||
|
|
||||||
class ExamGroupsForm(ExamGroupsFormMixin):
|
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||||
class Meta(ExamGroupsFormMixin.Meta):
|
class Meta(ExamGroupsFormMixin.Meta):
|
||||||
model = Exam
|
model = Exam
|
||||||
|
|
||||||
|
class SuggestIncorrectAnswerForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = AnatomyQuestion
|
||||||
|
fields = ["answer_suggest_incorrect"]
|
||||||
@@ -340,6 +340,9 @@ class Answer(models.Model):
|
|||||||
def get_answer_url_safe(self):
|
def get_answer_url_safe(self):
|
||||||
return urllib.parse.quote(self.answer_compare)
|
return urllib.parse.quote(self.answer_compare)
|
||||||
|
|
||||||
|
def get_constituent_words(self):
|
||||||
|
return self.answer_compare.split()
|
||||||
|
|
||||||
# def get_compare_string(self):
|
# def get_compare_string(self):
|
||||||
# s = self.answer.lower().strip()
|
# s = self.answer.lower().strip()
|
||||||
# s = s.translate(str.maketrans('', '', string.punctuation))
|
# s = s.translate(str.maketrans('', '', string.punctuation))
|
||||||
|
|||||||
@@ -15,7 +15,28 @@
|
|||||||
{% if question.answer_help %}
|
{% if question.answer_help %}
|
||||||
<details>
|
<details>
|
||||||
<summary>Marking help:</summary>
|
<summary>Marking help:</summary>
|
||||||
|
<div class="border border-secondary">
|
||||||
{{question.answer_help|safe}}
|
{{question.answer_help|safe}}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>Suggest incorrect answers:</summary>
|
||||||
|
|
||||||
|
{% for word in words_suggest_incorrect %}
|
||||||
|
<span>{{word}}<span class="user-select-none" _="on click if #id_answer_suggest_incorrect == null then
|
||||||
|
set btn to #answer-suggest-incorrect-button
|
||||||
|
js(btn) btn.click() end
|
||||||
|
repeat forever
|
||||||
|
if #id_answer_suggest_incorrect != null then break end
|
||||||
|
wait 0.1s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
put (#id_answer_suggest_incorrect's value + ',' + my @data-word) into #id_answer_suggest_incorrect's value then remove closest parent <span/>" data-word='{{word}}'>(+)</span>,</span>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div class="border border-secondary">
|
||||||
|
{% include "anatomy/question_detail.html#suggest-incorrect-form" %}
|
||||||
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'anatomy/base.html' %}
|
{% extends 'anatomy/base.html' %}
|
||||||
|
{% load partials %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% include 'anatomy/question_link_header.html' %}
|
{% include 'anatomy/question_link_header.html' %}
|
||||||
@@ -58,7 +58,14 @@
|
|||||||
Answer help: {{ question.answer_help|safe }}
|
Answer help: {{ question.answer_help|safe }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Answer suggest incorrect: {{ question.answer_suggest_incorrect }}
|
{% partialdef suggest-incorrect-form inline %}
|
||||||
|
<span id="incorrect-answers">
|
||||||
|
Answer suggest incorrect: {{ question.answer_suggest_incorrect }}
|
||||||
|
<button id="answer-suggest-incorrect-button" class="btn btn-sm" hx-get="{% url 'anatomy:question_suggest_incorrect_answers' question.pk %}"
|
||||||
|
hx-target="#incorrect-answers"
|
||||||
|
hx-swap="innerHTML">Edit suggestions</button>
|
||||||
|
</span>
|
||||||
|
{% endpartialdef %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Description: {{ question.description }}
|
Description: {{ question.description }}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{% load crispy_forms_tags %}
|
||||||
|
<form method="POST" class="post-form">{% csrf_token %}
|
||||||
|
{{ form | crispy}}
|
||||||
|
<button type="button" class="btn btn-sm" hx-post="{% url 'anatomy:question_suggest_incorrect_answers' question.id %}"
|
||||||
|
hx-target="#incorrect-answers"
|
||||||
|
hx-swap="innerHTML">Save</button>
|
||||||
|
<button type="button" _="on click remove closest <form/>" >Cancel</button>
|
||||||
|
</form>
|
||||||
@@ -33,6 +33,12 @@ urlpatterns = [
|
|||||||
views.question_save_annotation,
|
views.question_save_annotation,
|
||||||
name="question_save_annotation",
|
name="question_save_annotation",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"question/<int:pk>/suggest_incorrect_answers",
|
||||||
|
views.question_suggest_incorrect_answers,
|
||||||
|
name="question_suggest_incorrect_answers",
|
||||||
|
),
|
||||||
|
|
||||||
path("question/<int:pk>/answer/", views.answer_question, name="answer_question"),
|
path("question/<int:pk>/answer/", views.answer_question, name="answer_question"),
|
||||||
path(
|
path(
|
||||||
"question/<int:pk>/clone", views.QuestionClone.as_view(), name="question_clone"
|
"question/<int:pk>/clone", views.QuestionClone.as_view(), name="question_clone"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from .forms import (
|
|||||||
MarkAnatomyQuestionForm,
|
MarkAnatomyQuestionForm,
|
||||||
AnatomyQuestionForm,
|
AnatomyQuestionForm,
|
||||||
ExamForm,
|
ExamForm,
|
||||||
|
SuggestIncorrectAnswerForm,
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
AnatomyQuestion,
|
AnatomyQuestion,
|
||||||
@@ -345,6 +346,10 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
|
|||||||
incorrect_answers = question.answers.filter(status=Answer.MarkOptions.INCORRECT)
|
incorrect_answers = question.answers.filter(status=Answer.MarkOptions.INCORRECT)
|
||||||
|
|
||||||
answer_suggest_incorrect: list = []
|
answer_suggest_incorrect: list = []
|
||||||
|
|
||||||
|
words_present: set = set()
|
||||||
|
words_present_in_correct: set = set()
|
||||||
|
|
||||||
if review:
|
if review:
|
||||||
for ans in unmarked_user_answers:
|
for ans in unmarked_user_answers:
|
||||||
# duplicated from user answer model....
|
# duplicated from user answer model....
|
||||||
@@ -367,6 +372,22 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
|
|||||||
answer_suggest_incorrect.append(ans)
|
answer_suggest_incorrect.append(ans)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
for ans in correct_answers:
|
||||||
|
answer_words = set(ans.get_constituent_words())
|
||||||
|
words_present.update(answer_words)
|
||||||
|
words_present_in_correct.update(answer_words)
|
||||||
|
|
||||||
|
for ans in half_mark_answers:
|
||||||
|
answer_words = set(ans.get_constituent_words())
|
||||||
|
words_present.update(answer_words)
|
||||||
|
words_present_in_correct.update(answer_words)
|
||||||
|
|
||||||
|
for ans in incorrect_answers:
|
||||||
|
answer_words = set(ans.get_constituent_words())
|
||||||
|
words_present.update(answer_words)
|
||||||
|
|
||||||
|
words_suggest_incorrect = words_present - words_present_in_correct - set(question.answer_suggest_incorrect)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"anatomy/mark.html",
|
"anatomy/mark.html",
|
||||||
@@ -385,6 +406,7 @@ def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False):
|
|||||||
"incorrect_answers": incorrect_answers,
|
"incorrect_answers": incorrect_answers,
|
||||||
"unmarked_exam_answers_only": unmarked_exam_answers_only,
|
"unmarked_exam_answers_only": unmarked_exam_answers_only,
|
||||||
"answer_suggest_incorrect": answer_suggest_incorrect,
|
"answer_suggest_incorrect": answer_suggest_incorrect,
|
||||||
|
"words_suggest_incorrect": words_suggest_incorrect,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -748,6 +770,25 @@ class AnatomyQuestionView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
|||||||
|
|
||||||
# return qs
|
# return qs
|
||||||
|
|
||||||
|
@user_is_author_or_anatomy_checker
|
||||||
|
def question_suggest_incorrect_answers(request, pk):
|
||||||
|
question = get_object_or_404(AnatomyQuestion, pk=pk)
|
||||||
|
if request.method == "POST":
|
||||||
|
form = SuggestIncorrectAnswerForm(request.POST, instance=question)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
obj = form.save()
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
return render(request, "anatomy/question_detail.html#suggest-incorrect-form", {"question": question})
|
||||||
|
|
||||||
|
return HttpResponse("Invalid")
|
||||||
|
else:
|
||||||
|
form = SuggestIncorrectAnswerForm(instance=question)
|
||||||
|
|
||||||
|
return render(request, "anatomy/suggest_incorrect.html", {"form": form, "question": question})
|
||||||
|
|
||||||
|
return HttpResponse("Error", status=400)
|
||||||
|
|
||||||
@user_is_author_or_anatomy_checker
|
@user_is_author_or_anatomy_checker
|
||||||
def question_save_annotation(request, pk):
|
def question_save_annotation(request, pk):
|
||||||
|
|||||||
@@ -568,24 +568,6 @@ class SeriesImage(SeriesImageBase):
|
|||||||
help_text="Reference to the object that has replaced this one.",
|
help_text="Reference to the object that has replaced this one.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_file_size(self):
|
|
||||||
try:
|
|
||||||
return self.image.size
|
|
||||||
|
|
||||||
# We catch this for when the image does not exist
|
|
||||||
except FileNotFoundError:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_dicom_data(self):
|
|
||||||
try:
|
|
||||||
with pydicom.dcmread(self.image) as d:
|
|
||||||
return d
|
|
||||||
except InvalidDicomError:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_series(self):
|
|
||||||
return self.series
|
|
||||||
|
|
||||||
# def get_image_dicom_json(self, image_index):
|
# def get_image_dicom_json(self, image_index):
|
||||||
# try:
|
# try:
|
||||||
# with pydicom.dcmread(self.image) as ds:
|
# with pydicom.dcmread(self.image) as ds:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError
|
|||||||
|
|
||||||
|
|
||||||
import pydicom
|
import pydicom
|
||||||
|
import pydicom.errors
|
||||||
import dicognito.anonymizer
|
import dicognito.anonymizer
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
@@ -240,6 +241,24 @@ class SeriesImageBase(models.Model):
|
|||||||
ordering = ["position"]
|
ordering = ["position"]
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
def get_file_size(self):
|
||||||
|
try:
|
||||||
|
return self.image.size
|
||||||
|
|
||||||
|
# We catch this for when the image does not exist
|
||||||
|
except FileNotFoundError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_dicom_data(self):
|
||||||
|
try:
|
||||||
|
with pydicom.dcmread(self.image) as d:
|
||||||
|
return d
|
||||||
|
except pydicom.errors.InvalidDicomError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_series(self):
|
||||||
|
return self.series
|
||||||
|
|
||||||
def get_dicom_json(self):
|
def get_dicom_json(self):
|
||||||
try:
|
try:
|
||||||
json = pydicom.read_file(self.image).to_json()
|
json = pydicom.read_file(self.image).to_json()
|
||||||
|
|||||||
@@ -1525,6 +1525,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# #queryset=self.Answer.objects.filter(),
|
# #queryset=self.Answer.objects.filter(),
|
||||||
# ),
|
# ),
|
||||||
)
|
)
|
||||||
|
.order_by("examquestiondetail__sort_order")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
questions = exam.get_questions()
|
questions = exam.get_questions()
|
||||||
|
|||||||
Reference in New Issue
Block a user