Merge branch 'master' of ssh://161.35.163.87:/home/django/rad
This commit is contained in:
+4
-11
@@ -205,6 +205,7 @@ class Long(models.Model):
|
||||
#"feedback_image": [],
|
||||
#"annotations": [str(q.image_annotations)],
|
||||
"type": "long",
|
||||
"cached": False,
|
||||
}
|
||||
|
||||
return exam_question
|
||||
@@ -393,14 +394,10 @@ class LongSeries(models.Model):
|
||||
else:
|
||||
skipcount = skipcount + 1
|
||||
|
||||
print("skipped, no SliceLocation: {}".format(skipcount))
|
||||
print("skipped, no {}: {}".format(field, skipcount))
|
||||
|
||||
# ensure they are in the correct order
|
||||
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)
|
||||
@@ -632,12 +629,6 @@ class CidUserAnswer(models.Model):
|
||||
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):
|
||||
if self.score == "":
|
||||
return False
|
||||
@@ -645,4 +636,6 @@ class CidUserAnswer(models.Model):
|
||||
return True
|
||||
|
||||
def get_answer_score(self):
|
||||
if self.score == "":
|
||||
return ""
|
||||
return float(self.score)
|
||||
|
||||
@@ -4,13 +4,28 @@
|
||||
<div class="longs">
|
||||
<h2>Exam: {{ exam.name }}</h2>
|
||||
<h3>Candidate: {{ cid }}</h3>
|
||||
Answers:
|
||||
Scores:
|
||||
<ul>{% for score in answers_marks %}
|
||||
<li class="user-answer-li">Question {{forloop.counter}}</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>
|
||||
<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>
|
||||
<a href="{% url 'cid_scores' cid %}">Other exams</a>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ This series is not associated with any cases.
|
||||
<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_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>
|
||||
</div>
|
||||
{% for image in series.images.all %}
|
||||
|
||||
@@ -13,6 +13,7 @@ urlpatterns = [
|
||||
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_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>/delete", views.LongSeriesDelete.as_view(), name="long_series_delete"),
|
||||
# path("unchecked/", views.unchecked_list, name="unchecked_list"),
|
||||
|
||||
@@ -889,21 +889,27 @@ def exam_scores_cid_user(request, pk, sk):
|
||||
#answers_and_marks = []
|
||||
answers_marks = []
|
||||
#answers = []
|
||||
answer_text = []
|
||||
|
||||
for q in questions:
|
||||
# Get user answer
|
||||
user_answer = q.cid_user_answers.filter(cid=cid).first()
|
||||
|
||||
|
||||
|
||||
|
||||
if not user_answer or user_answer is None:
|
||||
# skip if no answer
|
||||
#answers_marks.append("")
|
||||
#answers.append("")
|
||||
answer_score = 4
|
||||
#ans = "Not answered"
|
||||
answer_text.append((("Not answered"),("Not answered"),("Not answered"),("Not answered"),("Not answered")))
|
||||
else:
|
||||
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:
|
||||
answer_score = 0
|
||||
#answers.append(ans)
|
||||
@@ -931,6 +937,7 @@ def exam_scores_cid_user(request, pk, sk):
|
||||
"answers_marks": answers_marks,
|
||||
"total_score": total_score,
|
||||
"max_score": max_score,
|
||||
"answer_text": answer_text
|
||||
#"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)
|
||||
|
||||
@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
|
||||
def long_series_order_upload_filename(request, 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)
|
||||
|
||||
question.recreate_json = False
|
||||
question.save()
|
||||
|
||||
return JsonResponse(question_json)
|
||||
|
||||
@login_required
|
||||
|
||||
+7
-1
@@ -1,5 +1,5 @@
|
||||
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
|
||||
|
||||
@@ -72,3 +72,9 @@ class RapidAdmin(VersionAdmin):
|
||||
admin.site.register(Rapid, RapidAdmin)
|
||||
|
||||
#tagulous.admin.register(Rapid.condition)
|
||||
|
||||
class CidUserAnswerAdmin(admin.ModelAdmin):
|
||||
exclude = []
|
||||
readonly_fields = ["created", "updated"]
|
||||
|
||||
admin.site.register(CidUserAnswer, CidUserAnswerAdmin)
|
||||
+14
-10
@@ -65,13 +65,13 @@ class AbnormalityForm(ModelForm):
|
||||
class RapidCreationDefaultForm(ModelForm):
|
||||
class Meta:
|
||||
model = RapidCreationDefault
|
||||
#fields = ["site"]
|
||||
# fields = ["site"]
|
||||
exclude = ["author"]
|
||||
|
||||
|
||||
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:
|
||||
# Django also includes a few javascript files necessary
|
||||
# for the operation of this form element. You need to
|
||||
@@ -84,11 +84,6 @@ class RapidForm(ModelForm):
|
||||
js = ["jsi18n.js", "tesseract.min.js"]
|
||||
|
||||
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"):
|
||||
# We get the 'initial' keyword argument or initialize it
|
||||
# as a dict if it didn't exist.
|
||||
@@ -97,6 +92,11 @@ class RapidForm(ModelForm):
|
||||
# a list of primary key for the selected data.
|
||||
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(
|
||||
required=False,
|
||||
queryset=Abnormality.objects.all(),
|
||||
@@ -117,7 +117,11 @@ class RapidForm(ModelForm):
|
||||
self.fields["laterality"] = ChoiceField(
|
||||
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):
|
||||
# Get the unsaved Pizza instance
|
||||
@@ -152,7 +156,7 @@ class RapidForm(ModelForm):
|
||||
"region",
|
||||
"laterality",
|
||||
"examination",
|
||||
#"site",
|
||||
# "site",
|
||||
"feedback",
|
||||
]
|
||||
# fields = ['question', 'feedback', 'subspecialty', 'references']
|
||||
@@ -185,7 +189,7 @@ AnswerFormSet = inlineformset_factory(
|
||||
},
|
||||
exclude=[],
|
||||
can_delete=True,
|
||||
extra=1,
|
||||
extra=0,
|
||||
max_num=10,
|
||||
)
|
||||
|
||||
|
||||
+17
-2
@@ -99,7 +99,7 @@ class Answer(models.Model):
|
||||
self.answer = self.answer.strip()
|
||||
|
||||
s = self.answer.lower()
|
||||
s = s.translate(str.maketrans('', '', string.punctuation))
|
||||
s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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):
|
||||
questions = self.exam_questions.all()
|
||||
|
||||
@@ -443,7 +458,7 @@ class CidUserAnswer(models.Model):
|
||||
self.answer = self.answer.strip()
|
||||
|
||||
s = self.answer.lower()
|
||||
s = s.translate(str.maketrans('', '', string.punctuation))
|
||||
s = s.translate(str.maketrans('', '', string.punctuation.replace("#", "")))
|
||||
|
||||
self.answer_compare = s
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<div class="rapids">
|
||||
<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>
|
||||
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">
|
||||
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>
|
||||
|
||||
+20
-7
@@ -355,6 +355,10 @@ class RapidUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
||||
return context
|
||||
|
||||
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.save()
|
||||
@@ -371,6 +375,14 @@ class RapidUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
||||
image_formset.save()
|
||||
answer_formset.instance = self.object
|
||||
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
|
||||
else:
|
||||
return super().form_invalid(form)
|
||||
@@ -529,15 +541,14 @@ def loadJsonAnswer(answer):
|
||||
|
||||
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 = False
|
||||
if posted_answer == "Normal":
|
||||
normal = True
|
||||
posted_answer = ""
|
||||
# We can ignore the first ab
|
||||
elif answer["qidn"] == "1" and posted_answer == "Abnormal":
|
||||
return False
|
||||
|
||||
|
||||
if not exiting_answers:
|
||||
ans = CidUserAnswer(answer=posted_answer, normal=normal, cid=answer["cid"])
|
||||
@@ -551,8 +562,10 @@ def loadJsonAnswer(answer):
|
||||
# Update an existing answer
|
||||
# should never be more than one (famous last words)
|
||||
ans = exiting_answers[0]
|
||||
ans.normal = normal
|
||||
ans.answer = posted_answer
|
||||
if answer["qidn"] == "1":
|
||||
ans.normal = normal
|
||||
elif answer["qidn"] == "2":
|
||||
ans.answer = posted_answer
|
||||
ans.full_clean()
|
||||
ans.save()
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
<a href="{% url 'anatomy:index'%}">Anatomy</a>
|
||||
<a href="{% url 'physics:index'%}">Physics</a>
|
||||
<a href="{% url 'rapids:index'%}">Rapids</a>
|
||||
{% if not request.user.is_authenticated %}
|
||||
<a href="{% url 'login'%}">Log in</a>
|
||||
{% endif %}
|
||||
{% if request.user.is_staff %}<a href="{% url 'admin:index'%}">Admin</a>{% endif %}
|
||||
<a href="{% url 'password_reset'%}">Reset password</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user