Merge branch 'master' of ssh://161.35.163.87:/home/django/rad

This commit is contained in:
Ross
2021-03-08 11:43:38 +00:00
11 changed files with 104 additions and 33 deletions
+4 -11
View File
@@ -205,6 +205,7 @@ class Long(models.Model):
#"feedback_image": [], #"feedback_image": [],
#"annotations": [str(q.image_annotations)], #"annotations": [str(q.image_annotations)],
"type": "long", "type": "long",
"cached": False,
} }
return exam_question return exam_question
@@ -393,14 +394,10 @@ class LongSeries(models.Model):
else: else:
skipcount = skipcount + 1 skipcount = skipcount + 1
print("skipped, no SliceLocation: {}".format(skipcount)) print("skipped, no {}: {}".format(field, skipcount))
# ensure they are in the correct order # ensure they are in the correct order
slices = sorted(slices, key=lambda s: s[field].value) slices = sorted(slices, key=lambda s: s[field].value)
#if field == "SliceLocation":
# slices = sorted(slices, key=lambda s: s.SliceLocation)
#elif field == "InstanceNumber":
# slices = sorted(slices, key=lambda s: s.InstanceNumber)
#print(slices) #print(slices)
@@ -632,12 +629,6 @@ class CidUserAnswer(models.Model):
ans = ans.strip() ans = ans.strip()
# def get_compare_string(self):
# # strip here should be unneccasry (providing clean is now working)
# s = self.answer.lower().strip()
# s = s.translate(str.maketrans('', '', string.punctuation))
# return s
def is_marked(self): def is_marked(self):
if self.score == "": if self.score == "":
return False return False
@@ -645,4 +636,6 @@ class CidUserAnswer(models.Model):
return True return True
def get_answer_score(self): def get_answer_score(self):
if self.score == "":
return ""
return float(self.score) return float(self.score)
+16 -1
View File
@@ -4,13 +4,28 @@
<div class="longs"> <div class="longs">
<h2>Exam: {{ exam.name }}</h2> <h2>Exam: {{ exam.name }}</h2>
<h3>Candidate: {{ cid }}</h3> <h3>Candidate: {{ cid }}</h3>
Answers: Scores:
<ul>{% for score in answers_marks %} <ul>{% for score in answers_marks %}
<li class="user-answer-li">Question {{forloop.counter}}</li> <li class="user-answer-li">Question {{forloop.counter}}</li>
<span class="user-answer-score user-answer-score-{{score}}"><pre>{{ans}}</pre> ({{score}})</span> <span class="user-answer-score user-answer-score-{{score}}"><pre>{{ans}}</pre> ({{score}})</span>
{% endfor %} {% endfor %}
</ul> </ul>
<br /> Total mark: {{ total_score }} / {{max_score}} <br /> Total mark: {{ total_score }} / {{max_score}}
<div>
<h4>Answers</h4>
<ul class="long-answer">{% for a,b,c,d,e in answer_text %}
<li class="user-answer-li"><b>Question {{forloop.counter}}</b></li>
<ul>
<li class="user-answer-li">Observation</br><pre>{{a}}</pre></li>
<li class="user-answer-li">Interpretation</br><pre>{{b}}</pre></li>
<li class="user-answer-li">Principle Diagnosis</br><pre>{{c}}</pre></li>
<li class="user-answer-li">Differential Diagnosis</br><pre>{{d}}</pre></li>
<li class="user-answer-li">Management</br><pre>{{e}}</pre></li>
</ul>
{% endfor %}
</ul>
</div>
<div> <div>
<a href="{% url 'cid_scores' cid %}">Other exams</a> <a href="{% url 'cid_scores' cid %}">Other exams</a>
</div> </div>
@@ -17,6 +17,7 @@ This series is not associated with any cases.
<div> <div>
<a href="{% url 'longs:long_series_order_dicom' pk=series.pk %}" title="orders dicom by slice location">Order dicoms by slice location</a> <a href="{% url 'longs:long_series_order_dicom' pk=series.pk %}" title="orders dicom by slice location">Order dicoms by slice location</a>
<a href="{% url 'longs:long_series_order_dicom_instance' pk=series.pk %}" title="orders dicom by instance number">Order dicoms by instance number</a> <a href="{% url 'longs:long_series_order_dicom_instance' pk=series.pk %}" title="orders dicom by instance number">Order dicoms by instance number</a>
<a href="{% url 'longs:long_series_order_dicom_SeriesInstanceUID' pk=series.pk %}" title="orders dicom by instance number">Order dicoms by SeriesInstanceUID</a>
<a href="{% url 'longs:long_series_order_upload_filename' pk=series.pk %}" title="orders dicom by uploaded filename">Order by uploaded filename</a> <a href="{% url 'longs:long_series_order_upload_filename' pk=series.pk %}" title="orders dicom by uploaded filename">Order by uploaded filename</a>
</div> </div>
{% for image in series.images.all %} {% for image in series.images.all %}
+1
View File
@@ -13,6 +13,7 @@ urlpatterns = [
path("series/<int:pk>", views.long_series_detail, name="long_series_detail"), path("series/<int:pk>", views.long_series_detail, name="long_series_detail"),
path("series/<int:pk>/order_dicom", views.long_series_order_dicom, name="long_series_order_dicom"), path("series/<int:pk>/order_dicom", views.long_series_order_dicom, name="long_series_order_dicom"),
path("series/<int:pk>/order_dicom_instance", views.long_series_order_dicom_instance, name="long_series_order_dicom_instance"), path("series/<int:pk>/order_dicom_instance", views.long_series_order_dicom_instance, name="long_series_order_dicom_instance"),
path("series/<int:pk>/order_dicom_SeriesInstanceUID", views.long_series_order_dicom_SeriesInstanceUID, name="long_series_order_dicom_SeriesInstanceUID"),
path("series/<int:pk>/order_upload_filename", views.long_series_order_upload_filename, name="long_series_order_upload_filename"), path("series/<int:pk>/order_upload_filename", views.long_series_order_upload_filename, name="long_series_order_upload_filename"),
path("series/<int:pk>/delete", views.LongSeriesDelete.as_view(), name="long_series_delete"), path("series/<int:pk>/delete", views.LongSeriesDelete.as_view(), name="long_series_delete"),
# path("unchecked/", views.unchecked_list, name="unchecked_list"), # path("unchecked/", views.unchecked_list, name="unchecked_list"),
+20
View File
@@ -889,21 +889,27 @@ def exam_scores_cid_user(request, pk, sk):
#answers_and_marks = [] #answers_and_marks = []
answers_marks = [] answers_marks = []
#answers = [] #answers = []
answer_text = []
for q in questions: for q in questions:
# Get user answer # Get user answer
user_answer = q.cid_user_answers.filter(cid=cid).first() user_answer = q.cid_user_answers.filter(cid=cid).first()
if not user_answer or user_answer is None: if not user_answer or user_answer is None:
# skip if no answer # skip if no answer
#answers_marks.append("") #answers_marks.append("")
#answers.append("") #answers.append("")
answer_score = 4 answer_score = 4
#ans = "Not answered" #ans = "Not answered"
answer_text.append((("Not answered"),("Not answered"),("Not answered"),("Not answered"),("Not answered")))
else: else:
answer_score = user_answer.get_answer_score() answer_score = user_answer.get_answer_score()
answer_text.append((user_answer.answer_observations, user_answer.answer_interpretation, user_answer.answer_principle_diagnosis, user_answer.answer_differential_diagnosis, user_answer.answer_management))
if not exam.publish_results: if not exam.publish_results:
answer_score = 0 answer_score = 0
#answers.append(ans) #answers.append(ans)
@@ -931,6 +937,7 @@ def exam_scores_cid_user(request, pk, sk):
"answers_marks": answers_marks, "answers_marks": answers_marks,
"total_score": total_score, "total_score": total_score,
"max_score": max_score, "max_score": max_score,
"answer_text": answer_text
#"answers_and_marks": answers_and_marks, #"answers_and_marks": answers_and_marks,
}, },
) )
@@ -955,6 +962,16 @@ def long_series_order_dicom_instance(request, pk):
return redirect("longs:long_series_detail", pk=pk) return redirect("longs:long_series_detail", pk=pk)
@login_required
def long_series_order_dicom_SeriesInstanceUID(request, pk):
series = get_object_or_404(LongSeries, pk=pk)
try:
series.order_by_dicom("SeriesInstanceUID")
except:
return HttpResponse('<h1>Series does not appear to contain dicoms (or field SeriesInstanceUID)</h1>')
return redirect("longs:long_series_detail", pk=pk)
@login_required @login_required
def long_series_order_upload_filename(request, pk): def long_series_order_upload_filename(request, pk):
series = get_object_or_404(LongSeries, pk=pk) series = get_object_or_404(LongSeries, pk=pk)
@@ -986,6 +1003,9 @@ def question_json(request, pk):
cache.set("{}_question_json_{}".format("longs", pk), question_json, 3600) cache.set("{}_question_json_{}".format("longs", pk), question_json, 3600)
question.recreate_json = False
question.save()
return JsonResponse(question_json) return JsonResponse(question_json)
@login_required @login_required
+7 -1
View File
@@ -1,5 +1,5 @@
from django.contrib import admin from django.contrib import admin
from .models import Rapid, RapidImage, Examination, Site, Abnormality, Region, Note, RapidCreationDefault, Answer, Exam from .models import Rapid, RapidImage, Examination, Site, Abnormality, Region, Note, RapidCreationDefault, Answer, Exam, CidUserAnswer
import tagulous.admin import tagulous.admin
@@ -72,3 +72,9 @@ class RapidAdmin(VersionAdmin):
admin.site.register(Rapid, RapidAdmin) admin.site.register(Rapid, RapidAdmin)
#tagulous.admin.register(Rapid.condition) #tagulous.admin.register(Rapid.condition)
class CidUserAnswerAdmin(admin.ModelAdmin):
exclude = []
readonly_fields = ["created", "updated"]
admin.site.register(CidUserAnswer, CidUserAnswerAdmin)
+14 -10
View File
@@ -65,13 +65,13 @@ class AbnormalityForm(ModelForm):
class RapidCreationDefaultForm(ModelForm): class RapidCreationDefaultForm(ModelForm):
class Meta: class Meta:
model = RapidCreationDefault model = RapidCreationDefault
#fields = ["site"] # fields = ["site"]
exclude = ["author"] exclude = ["author"]
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))
class Media: class Media:
# Django also includes a few javascript files necessary # Django also includes a few javascript files necessary
# for the operation of this form element. You need to # for the operation of this form element. You need to
@@ -84,11 +84,6 @@ class RapidForm(ModelForm):
js = ["jsi18n.js", "tesseract.min.js"] js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
ModelForm.__init__(self, *args, **kwargs)
super(RapidForm, self).__init__(*args, **kwargs)
# self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
# self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
if kwargs.get("instance"): if kwargs.get("instance"):
# We get the 'initial' keyword argument or initialize it # We get the 'initial' keyword argument or initialize it
# as a dict if it didn't exist. # as a dict if it didn't exist.
@@ -97,6 +92,11 @@ class RapidForm(ModelForm):
# a list of primary key for the selected data. # a list of primary key for the selected data.
initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()] initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()]
ModelForm.__init__(self, *args, **kwargs)
super(RapidForm, self).__init__(*args, **kwargs)
# self.fields['question'].widget.attrs = {'class': 'question-form', 'rows': 10, 'cols': 100}
# self.fields['feedback'].widget.attrs = {'class': 'feedback-form', 'rows': 10, 'cols': 100}
self.fields["abnormality"] = ModelMultipleChoiceField( self.fields["abnormality"] = ModelMultipleChoiceField(
required=False, required=False,
queryset=Abnormality.objects.all(), queryset=Abnormality.objects.all(),
@@ -117,7 +117,11 @@ class RapidForm(ModelForm):
self.fields["laterality"] = ChoiceField( self.fields["laterality"] = ChoiceField(
choices=Rapid.LATERALITY_CHOICES, required=False, widget=RadioSelect() choices=Rapid.LATERALITY_CHOICES, required=False, widget=RadioSelect()
) )
self.fields["exams"] = ModelMultipleChoiceField(
required=False,
queryset=Exam.objects.all(),
widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False),
)
def save(self, commit=True): def save(self, commit=True):
# Get the unsaved Pizza instance # Get the unsaved Pizza instance
@@ -152,7 +156,7 @@ class RapidForm(ModelForm):
"region", "region",
"laterality", "laterality",
"examination", "examination",
#"site", # "site",
"feedback", "feedback",
] ]
# fields = ['question', 'feedback', 'subspecialty', 'references'] # fields = ['question', 'feedback', 'subspecialty', 'references']
@@ -185,7 +189,7 @@ AnswerFormSet = inlineformset_factory(
}, },
exclude=[], exclude=[],
can_delete=True, can_delete=True,
extra=1, extra=0,
max_num=10, max_num=10,
) )
+17 -2
View File
@@ -99,7 +99,7 @@ class Answer(models.Model):
self.answer = self.answer.strip() self.answer = self.answer.strip()
s = self.answer.lower() s = self.answer.lower()
s = s.translate(str.maketrans('', '', string.punctuation)) s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
self.answer_compare = s self.answer_compare = s
@@ -352,6 +352,21 @@ class Exam(ExamBase):
help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", default=2100 help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", default=2100
) )
def get_normal_abnormal_breakdown(self):
# Inefficient but more extendible
questions = self.exam_questions.all()
normal = []
abnormal = []
for q in questions:
if q.normal:
normal.append(q)
else:
abnormal.append(q)
return len(normal)
def get_exam_json(self): def get_exam_json(self):
questions = self.exam_questions.all() questions = self.exam_questions.all()
@@ -443,7 +458,7 @@ class CidUserAnswer(models.Model):
self.answer = self.answer.strip() self.answer = self.answer.strip()
s = self.answer.lower() s = self.answer.lower()
s = s.translate(str.maketrans('', '', string.punctuation)) s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
self.answer_compare = s self.answer_compare = s
+2 -1
View File
@@ -6,7 +6,8 @@
<div class="rapids"> <div class="rapids">
<a href="{% url 'admin:rapids_exam_change' exam.id %}" title="Edit the Exam using the admin interface">Admin Edit</a> <a href="{% url 'admin:rapids_exam_change' exam.id %}" title="Edit the Exam using the admin interface">Admin Edit</a>
<h1>Exam: {{ exam.name }}</h1> <h1>Exam: {{ exam.name }}</h1>
This exam has {{question_number}} questions. Time limit: {{exam.time_limit}} seconds. This exam has {{question_number}} questions. Time limit: {{exam.time_limit}} seconds.<br/>
Normal {{ exam.get_normal_abnormal_breakdown }}
<div class="parent-help" title="Click to enable / disable the exam"> <div class="parent-help" title="Click to enable / disable the exam">
Exam active: <input type="checkbox" id="exam-active-switch" {% if exam.active %}checked{% endif %}> <span class="help-text">[When checked the exam will be available to take in the test system]</span> Exam active: <input type="checkbox" id="exam-active-switch" {% if exam.active %}checked{% endif %}> <span class="help-text">[When checked the exam will be available to take in the test system]</span>
+18 -5
View File
@@ -355,6 +355,10 @@ class RapidUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
return context return context
def form_valid(self, form): def form_valid(self, form):
# save exam orders (there must be a better way to do this)
exam_orders = {}
for exam in self.object.exams.all():
exam_orders[exam] = list(exam.exam_questions.all())
self.object = form.save(commit=False) self.object = form.save(commit=False)
self.object.save() self.object.save()
@@ -371,6 +375,14 @@ class RapidUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
image_formset.save() image_formset.save()
answer_formset.instance = self.object answer_formset.instance = self.object
answer_formset.save() answer_formset.save()
# restore exam orders
for exam in self.object.exams.all():
if exam in exam_orders and self.object in exam_orders[exam]:
print(exam_orders[exam])
exam.exam_questions.set(exam_orders[exam])
exam.save()
return response return response
else: else:
return super().form_invalid(form) return super().form_invalid(form)
@@ -529,15 +541,14 @@ def loadJsonAnswer(answer):
posted_answer = answer["ans"] posted_answer = answer["ans"]
# qidn 1 does not hold answer data (but should always arrive prior to 2)
if answer["qidn"] == "1":
posted_answer = ""
# Normal answers are just posted with the answer "Normal" # Normal answers are just posted with the answer "Normal"
normal = False normal = False
if posted_answer == "Normal": if posted_answer == "Normal":
normal = True normal = True
posted_answer = ""
# We can ignore the first ab
elif answer["qidn"] == "1" and posted_answer == "Abnormal":
return False
if not exiting_answers: if not exiting_answers:
ans = CidUserAnswer(answer=posted_answer, normal=normal, cid=answer["cid"]) ans = CidUserAnswer(answer=posted_answer, normal=normal, cid=answer["cid"])
@@ -551,7 +562,9 @@ def loadJsonAnswer(answer):
# Update an existing answer # Update an existing answer
# should never be more than one (famous last words) # should never be more than one (famous last words)
ans = exiting_answers[0] ans = exiting_answers[0]
if answer["qidn"] == "1":
ans.normal = normal ans.normal = normal
elif answer["qidn"] == "2":
ans.answer = posted_answer ans.answer = posted_answer
ans.full_clean() ans.full_clean()
ans.save() ans.save()
+2
View File
@@ -6,7 +6,9 @@
<a href="{% url 'anatomy:index'%}">Anatomy</a> <a href="{% url 'anatomy:index'%}">Anatomy</a>
<a href="{% url 'physics:index'%}">Physics</a> <a href="{% url 'physics:index'%}">Physics</a>
<a href="{% url 'rapids:index'%}">Rapids</a> <a href="{% url 'rapids:index'%}">Rapids</a>
{% if not request.user.is_authenticated %}
<a href="{% url 'login'%}">Log in</a> <a href="{% url 'login'%}">Log in</a>
{% endif %}
{% if request.user.is_staff %}<a href="{% url 'admin:index'%}">Admin</a>{% endif %} {% if request.user.is_staff %}<a href="{% url 'admin:index'%}">Admin</a>{% endif %}
<a href="{% url 'password_reset'%}">Reset password</a> <a href="{% url 'password_reset'%}">Reset password</a>
</div> </div>