start anatomy creation forms

This commit is contained in:
Ross
2020-12-06 21:26:23 +00:00
parent ad793cd0af
commit 994d0ec677
10 changed files with 768 additions and 87 deletions
+4
View File
@@ -7,6 +7,7 @@ from .models import (
CidUserAnswer,
Exam,
Modality,
Region,
BodyPart,
Structure,
)
@@ -42,6 +43,8 @@ class AnatomyAdmin(VersionAdmin):
ExamInline,
]
exclude = [ "image_annotations" ]
admin.site.register(AnatomyQuestion, AnatomyAdmin)
admin.site.register(Examination)
@@ -49,6 +52,7 @@ admin.site.register(QuestionType)
admin.site.register(CidUserAnswer)
admin.site.register(Exam)
admin.site.register(Modality)
admin.site.register(Region)
admin.site.register(BodyPart)
admin.site.register(Structure)
+98 -5
View File
@@ -1,16 +1,109 @@
from django import forms
from django.forms import (
Form,
ModelForm,
ModelMultipleChoiceField,
ModelChoiceField,
ChoiceField,
CharField,
)
from django.forms import inlineformset_factory
from .models import CidUserAnswer, AnatomyQuestion
from .models import (
Answer,
CidUserAnswer,
AnatomyQuestion,
Examination,
BodyPart,
Structure,
Region,
Modality,
QuestionType,
)
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, TextInput
class AnatomyAnswerForm(forms.ModelForm):
class AnatomyAnswerForm(ModelForm):
class Meta:
model = CidUserAnswer
fields = ("answer",)
class MarkAnatomyQuestionForm(forms.Form):
class MarkAnatomyQuestionForm(Form):
# correct = forms.CharField(required=False)
# half_correct = forms.CharField(required=False)
# incorrect = forms.CharField(required=False)
marked_answers = forms.CharField(required=False)
marked_answers = CharField(required=False)
class AnatomyQuestionForm(ModelForm):
class Media:
# Django also includes a few javascript files necessary
# for the operation of this form element. You need to
# include <script src="/admin/jsi18n"></script>
# in the template.
css = {
"all": ["css/widgets.css"],
}
# Adding this javascript is crucial
js = ["jsi18n.js", "tesseract.min.js"]
def __init__(self, *args, **kwargs):
super(AnatomyQuestionForm, 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["question_type"] = ModelChoiceField(
required=True,
queryset=QuestionType.objects.all(),
initial=1,
)
self.fields["region"] = ModelChoiceField(
required=False,
queryset=Region.objects.all(),
)
self.fields["modality"] = ModelChoiceField(
required=True,
queryset=Modality.objects.all(),
)
self.fields["structure"] = ModelChoiceField(
required=False,
queryset=Structure.objects.all(),
)
self.fields["examination"] = ModelChoiceField(
required=False,
queryset=Examination.objects.all(),
)
self.fields["body_part"] = ModelChoiceField(
required=False,
queryset=BodyPart.objects.all(),
)
class Meta:
model = AnatomyQuestion
fields = ["question_type", "image", "description"]
widgets = {}
AnswerFormSet = inlineformset_factory(
AnatomyQuestion,
Answer,
fields=["answer", "status"],
widgets={
"answer": TextInput(
attrs={"required": "true", "minlength": 2, "maxlength": 500}
)
},
exclude=[],
can_delete=True,
extra=1,
max_num=10,
field_classes="testing",
)
@@ -0,0 +1,26 @@
# Generated by Django 3.1.3 on 2020-12-06 17:16
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('anatomy', '0009_anatomyquestion_image_annotations'),
]
operations = [
migrations.CreateModel(
name='Region',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('region', models.CharField(max_length=200)),
],
),
migrations.AddField(
model_name='anatomyquestion',
name='region',
field=models.ForeignKey(blank=True, help_text='Region the image covers', null=True, on_delete=django.db.models.deletion.SET_NULL, to='anatomy.region'),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 3.1.3 on 2020-12-06 18:12
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('anatomy', '0010_auto_20201206_1716'),
]
operations = [
migrations.AddField(
model_name='anatomyquestion',
name='author',
field=models.ManyToManyField(blank=True, help_text='Author(s) of question', related_name='anatomy_authored_questions', to=settings.AUTH_USER_MODEL),
),
]
+134 -79
View File
@@ -45,6 +45,13 @@ class Structure(models.Model):
return self.structure
class Region(models.Model):
region = models.CharField(max_length=200)
def __str__(self):
return self.region
class Modality(models.Model):
modality = models.CharField(max_length=200)
@@ -63,46 +70,62 @@ class AnatomyQuestion(models.Model):
# author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
# image = models.ImageField()
# feedback = models.TextField(null=True)
question_type = models.ForeignKey(QuestionType,
on_delete=models.SET_NULL,
null=True)
question_type = models.ForeignKey(
QuestionType, on_delete=models.SET_NULL, null=True
)
image = models.ImageField(upload_to=image_directory_path,
storage=image_storage)
image = models.ImageField(upload_to=image_directory_path, storage=image_storage)
image_annotations = models.TextField(blank=True, help_text="Stores a JSON representation of annotations to be applied by cornerstonetools")
image_annotations = models.TextField(
blank=True,
help_text="Stores a JSON representation of annotations to be applied by cornerstonetools",
)
description = models.CharField(
max_length=400,
help_text=
"Short description of the image e.g. 'Sagittal CT Chest, Abdomen & Pelvis', will be displayed as the title."
help_text="Short description of the image e.g. 'Sagittal CT Chest, Abdomen & Pelvis', will be displayed as the title.",
)
examination = models.ForeignKey(Examination,
on_delete=models.SET_NULL,
null=True,
blank=True)
modality = models.ForeignKey(Modality,
on_delete=models.SET_NULL,
null=True,
help_text="Modality of the image")
body_part = models.ForeignKey(BodyPart,
on_delete=models.SET_NULL,
null=True,
blank=True)
structure = models.ForeignKey(Structure,
on_delete=models.SET_NULL,
null=True,
blank=True)
examination = models.ForeignKey(
Examination, on_delete=models.SET_NULL, null=True, blank=True
)
modality = models.ForeignKey(
Modality,
on_delete=models.SET_NULL,
null=True,
help_text="Modality of the image",
)
region = models.ForeignKey(
Region,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Region the image covers",
)
body_part = models.ForeignKey(
BodyPart, on_delete=models.SET_NULL, null=True, blank=True
)
structure = models.ForeignKey(
Structure, on_delete=models.SET_NULL, null=True, blank=True
)
created_date = models.DateTimeField(default=timezone.now)
open_access = models.BooleanField(help_text="If an question should be freely available to browse",
default=True)
open_access = models.BooleanField(
help_text="If an question should be freely available to browse", default=True
)
author = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
help_text="Author(s) of question",
related_name="anatomy_authored_questions",
)
def __str__(self):
# Get first answer
return "{} [{}, {}]".format(self.GetPrimaryAnswer(), self.modality,
self.structure)
return "{} [{}, {}]".format(
self.GetPrimaryAnswer(), self.modality, self.structure
)
# Get associated exams
e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e)
@@ -110,18 +133,24 @@ class AnatomyQuestion(models.Model):
if a is None:
return "({})".format(exams)
else:
return "{} ({}) [{}, {}]".format(a.answer, exams, self.modality,
self.structure)
return "{} ({}) [{}, {}]".format(
a.answer, exams, self.modality, self.structure
)
def get_absolute_url(self):
return reverse("anatomy:question_detail", kwargs={"pk": self.pk})
def GetPrimaryAnswer(self):
return self.answers.all().first().answer
if len(self.answers.all()) > 0:
return self.answers.all().first().answer
else:
return "None yet..."
def GetExams(self):
e = self.exams.all().values_list("name", flat=True)
exams = ", ".join(e)
return exams
def GetUnmarkedAnswersString(self):
unmarked_answers = self.GetUnmarkedAnswers()
@@ -129,11 +158,14 @@ class AnatomyQuestion(models.Model):
return "No answers to mark"
return format_html(
"<span class='warn'>{} answer unmarked:</span> {}".format(
len(unmarked_answers), ", ".join(unmarked_answers)))
len(unmarked_answers), ", ".join(unmarked_answers)
)
)
def GetUnmarkedAnswers(self):
user_answers = set(
[i.get_compare_string() for i in self.cid_user_answers.all()])
[i.get_compare_string() for i in self.cid_user_answers.all()]
)
unmarked_answers = user_answers - self.GetMarkedAnswers()
return unmarked_answers
@@ -142,30 +174,40 @@ class AnatomyQuestion(models.Model):
return len(self.GetUnmarkedAnswers())
def GetMarkedAnswers(self):
return set([i.get_compare_string() for i in self.answers.all() if i.status != i.MarkOptions.UNMARKED])
return set(
[
i.get_compare_string()
for i in self.answers.all()
if i.status != i.MarkOptions.UNMARKED
]
)
correct_answers = set([i.answer.lower() for i in self.answers.all()])
half_mark_answers = set(
[i.answer.lower() for i in self.half_mark_answers.all()])
[i.answer.lower() for i in self.half_mark_answers.all()]
)
incorrect_answers = set(
[i.answer.lower() for i in self.incorrect_answers.all()])
[i.answer.lower() for i in self.incorrect_answers.all()]
)
marked_answers = correct_answers | half_mark_answers | incorrect_answers
return marked_answers
class Answer(models.Model):
question = models.ForeignKey(AnatomyQuestion,
related_name="answers",
on_delete=models.CASCADE)
question = models.ForeignKey(
AnatomyQuestion, related_name="answers", on_delete=models.CASCADE
)
answer = models.CharField(max_length=500)
class MarkOptions(models.TextChoices):
UNMARKED = '', _('Unmarked')
INCORRECT = '0', _('Incorrect')
HALF_MARK = '1', _('Half mark')
CORRECT = '2', _('Correct')
UNMARKED = "", _("Unmarked")
INCORRECT = "0", _("Incorrect")
HALF_MARK = "1", _("Half mark")
CORRECT = "2", _("Correct")
status = models.CharField(max_length=1, choices=MarkOptions.choices, default=MarkOptions.UNMARKED)
status = models.CharField(
max_length=1, choices=MarkOptions.choices, default=MarkOptions.UNMARKED
)
def __str__(self):
return self.answer
@@ -178,7 +220,7 @@ class Answer(models.Model):
return self.answer.lower()
#class HalfMarkAnswers(models.Model):
# class HalfMarkAnswers(models.Model):
# question = models.ForeignKey(AnatomyQuestion,
# related_name="half_mark_answers",
# on_delete=models.CASCADE)
@@ -192,7 +234,7 @@ class Answer(models.Model):
# self.answer.strip()
#
#
#class IncorrectAnswers(models.Model):
# class IncorrectAnswers(models.Model):
# question = models.ForeignKey(AnatomyQuestion,
# related_name="incorrect_answers",
# on_delete=models.CASCADE)
@@ -208,14 +250,17 @@ class Answer(models.Model):
class Exam(models.Model):
name = models.CharField(max_length=200)
exam_questions = SortedManyToManyField(AnatomyQuestion,
related_name="exams", blank="true")
exam_questions = SortedManyToManyField(
AnatomyQuestion, related_name="exams", blank="true"
)
active = models.BooleanField(help_text="If an exam should be available",
default=True)
active = models.BooleanField(
help_text="If an exam should be available", default=True
)
recreate_json = models.BooleanField(help_text="If the json cache needs updating",
default=False)
recreate_json = models.BooleanField(
help_text="If the json cache needs updating", default=False
)
def __str__(self):
return self.name
@@ -224,26 +269,24 @@ class Exam(models.Model):
return self.name
def get_json_url(self):
return reverse('anatomy:exam_json', args=(self.pk, ))
return reverse("anatomy:exam_json", args=(self.pk,))
class UserAnswer(models.Model):
question = models.ForeignKey(AnatomyQuestion,
related_name="user_answers",
on_delete=models.CASCADE)
question = models.ForeignKey(
AnatomyQuestion, related_name="user_answers", on_delete=models.CASCADE
)
answer = models.CharField(max_length=500, blank=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL
)
cid = models.IntegerField(blank=True, null=True)
# Each user answer is associated with a particular exam
exam = models.ForeignKey(Exam,
related_name="exam",
on_delete=models.CASCADE,
null=True)
exam = models.ForeignKey(
Exam, related_name="exam", on_delete=models.CASCADE, null=True
)
flagged = models.BooleanField(default=False)
@@ -255,8 +298,9 @@ class UserAnswer(models.Model):
exam = self.exam
except (Exam.DoesNotExist, KeyError) as e:
exam = "None"
return "{}/{}/{}: {}".format(exam, self.cid, self.question.GetPrimaryAnswer(),
self.answer)
return "{}/{}/{}: {}".format(
exam, self.cid, self.question.GetPrimaryAnswer(), self.answer
)
def clean(self):
if self.answer:
@@ -265,18 +309,18 @@ class UserAnswer(models.Model):
class CidUserAnswer(models.Model):
"""User answers by candidate"""
question = models.ForeignKey(AnatomyQuestion,
related_name="cid_user_answers",
on_delete=models.CASCADE)
question = models.ForeignKey(
AnatomyQuestion, related_name="cid_user_answers", on_delete=models.CASCADE
)
answer = models.CharField(max_length=500, blank=True)
cid = models.IntegerField(blank=True, null=True)
# Each user answer is associated with a particular exam
exam = models.ForeignKey(Exam,
related_name="cid_user_answers",
on_delete=models.CASCADE,
null=True)
exam = models.ForeignKey(
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
@@ -286,8 +330,9 @@ class CidUserAnswer(models.Model):
exam = self.exam
except (Exam.DoesNotExist, KeyError) as e:
exam = "None"
return "{}/{}/{}: {}".format(exam, self.cid, self.question.GetPrimaryAnswer(),
self.answer)
return "{}/{}/{}: {}".format(
exam, self.cid, self.question.GetPrimaryAnswer(), self.answer
)
def clean(self):
if self.answer:
@@ -299,9 +344,19 @@ class CidUserAnswer(models.Model):
def get_answer_score(self):
q = self.question
ans = self.answer
if q.answers.filter(answer__iexact=ans, status=Answer.MarkOptions.CORRECT).first() is not None:
if (
q.answers.filter(
answer__iexact=ans, status=Answer.MarkOptions.CORRECT
).first()
is not None
):
mark = 2
elif q.answers.filter(answer__iexact=ans, status=Answer.MarkOptions.HALF_MARK).first() is not None:
elif (
q.answers.filter(
answer__iexact=ans, status=Answer.MarkOptions.HALF_MARK
).first()
is not None
):
mark = 1
else:
mark = 0
+109
View File
@@ -166,4 +166,113 @@ button a {
#save-annotations {
position: absolute;
left: 0;
}
.drop-target {
border: 1px dashed white;
}
#drop-container {
width: 100%;
width: 100vw;
height: 100px;
text-align: center;
font-size: larger;
position: sticky;
bottom: 0;
left: 0%;
right: 50%;
margin-left: -50vw;
margin-right: -50vw;
}
.submit-button {
position: sticky;
bottom: 0;
}
#feedback-drop-target {
float: right;
height: 100%;
width: 30%;
}
.drop-target-active {
background-color: #427aa1;
}
#drop-filenames span {
font-size: xx-small;
color: gray;
padding-right: 1em;
}
.image-formset {
border: 1px solid white;
}
select, input {
background-color: #05668d;
color: white;
border: none;
}
table.table {
color: white;
}
#view-filter-options {
float: right;
}
.rapid-img {
width: 50%;
}
.feedback-img {
border: 1px solid #05668d;
}
img.uploading {
margin-top: -20px;
height: 75%;
float: right;
}
img.uploading:hover {
height: 95%;
position: fixed;
top: 50%;
left: 50%;
/* bring your own prefixes */
transform: translate(-50%, -50%)
}
.image-ident-warning {
border: 2px solid red;
}
.image-ident-loading {
background: rgba(142, 68, 173, 1);
box-shadow: 0 0 0 0 rgba(142, 68, 173, 1);
animation: pulse-purple 2s infinite;
}
@keyframes pulse-purple {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(142, 68, 173, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba(142, 68, 173, 0);
}
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(142, 68, 173, 0);
}
}
@@ -0,0 +1,260 @@
{% extends "anatomy/base.html" %}
{% load static %}
{% block js %}
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
<script type="text/javascript">
function showEditPopup(url) {
var win = window.open(url, "Edit",
'height=500,width=800,resizable=yes,scrollbars=yes');
return false;
}
function showAddPopup(triggeringLink) {
var name = triggeringLink.id.replace(/^add_/, '');
href = triggeringLink.href;
var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
win.focus();
return false;
}
function closePopup(win, newID, newRepr, id) {
console.log(id)
$(id + "_to").append('<option value=' + newID + ' title=' + newRepr + ' >' + newRepr + '</option>')
win.close();
}
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 () {
$("#add_abnormality").appendTo($("label[for='id_abnormality']"));
$("#add_examination").appendTo($("label[for='id_examination']"));
$("#add_region").appendTo($("label[for='id_region']"));
dropContainer = document.getElementById("drop-container");
dropContainer.ondragover = function (evt) {
evt.preventDefault();
evt.stopPropagation();
};
dropContainer.ondragenter = function (evt) {
$(evt.target).addClass("drop-target-active")
evt.preventDefault();
evt.stopPropagation();
};
dropContainer.ondragleave = function (evt) {
$(evt.target).removeClass("drop-target-active")
evt.preventDefault();
evt.stopPropagation();
};
dropContainer.ondrop = function (evt) {
console.log("drop event, ", evt);
$(evt.target).removeClass("drop-target-active");
// Get all input elements
input = document.getElementById("id_image");
//fileInput = document.getElementById("id_images-0-image");
f = evt.dataTransfer.files[0]
let dT = new DataTransfer();
dT.clearData();
dT.items.add(f);
input.files = dT.files;
ocr(input);
// Make sure we have enough input targets
//input_diff = (evt.dataTransfer.files.length - inputs.length)
//console.log("diff", input_diff)
//if (input_diff > 0) {
// for (let j = 0; j < input_diff; j++) {
// add_input_form()
// }
// // Need to make sure we include the new ones...
// inputs = $("#rapid-form input[type=file][id^=id_images-]");
//}
// Loop through each dropped file and try to assign to an
// input element
//[...evt.dataTransfer.files].forEach((f) => {
// let dT = new DataTransfer();
// dT.clearData();
// dT.items.add(f);
// for (let i = 0; i < inputs.length; i++) {
// el = inputs.get(i)
// if (el.files.length == 0) {
// el.files = dT.files;
// ocr(el)
// if (evt.target.id == "feedback-drop-target") {
// arr = el.name.split("-");
// arr.splice(2, 1, "feedback_image");
// feedback_el_name = arr.join("-");
// $(`[name=${feedback_el_name}]`).prop("checked", true);
// }
// return false
// }
// }
//})
// // If you want to use some of the dropped files
// const dT = new DataTransfer();
// dT.items.add(evt.dataTransfer.files[0]);
// dT.items.add(evt.dataTransfer.files[3]);
// fileInput.files = dT.files;
evt.preventDefault();
evt.stopPropagation();
updateFileList();
};
$('#add_more').click(() => { add_input_form() });
function updateFileList() {
$("#drop-filenames").empty()
$("#id_image").each((n, el) => {
console.log(el);
if (el.files.length > 0) {
extra_class = " image-ident-loading";
if ($(el).hasClass("image-ident-warning")) {
extra_class = " image-ident-warning"
} else if ($(el).hasClass("image-ident-ok")) {
extra_class = " image-ident-ok"
}
$("#drop-filenames").append(
`<span>${n}: ${el.files[0].name}</span><img class='uploading${extra_class}' data-input-id='${el.id}' src=${URL.createObjectURL(el.files[0])}>`
)
}
})
}
$("input[type=file]").on('change', function () {
$(this).removeClass("image-ident-warning");
$(this).removeClass("image-ident-ok");
updateFileList();
console.log("input change1");
console.log("input change", this);
ocr(this);
});
//document.getElementById("id_question_type").selectedIndex = 1;
answer_1_status = document.getElementById("id_answers-0-status").selectedIndex;
if (answer_1_status == 0) {
document.getElementById("id_answers-0-status").selectedIndex = 3;
}
});
function ocr(input) {
// Tesseract.recognize(f, "eng",{ logger: m => console.log(m) }
// ).then(({ data: { text } }) => {
// console.log(text);
// l = text.toLowerCase();
// if (l.includes("accesion") || l.includes("number") || l.search(/(REF|RK9|RH8|RA9)\d+/g)) {
// console.log("SHIT", this);
// }
// })
const worker = Tesseract.createWorker({
workerPath: '{% static "worker.min.js" %}',
langPath: '{% static "" %}',
//langPath: '{{ STATIC_URL }}',
corePath: '{% static "tesseract-core.wasm.js" %}',
});
const url = URL.createObjectURL(input.files[0]);
let img = new Image;
img.src = url;
img.onload = function () {
width = img.width;
// const rectangle = { left: 0, top: 0, width: width, height: 10 }
(async () => {
await worker.load();
await worker.loadLanguage('eng');
await worker.initialize('eng');
// const { data: { text } } = await worker.recognize(input.files[0], { rectangle });
const { data: { text } } = await worker.recognize(input.files[0]);
//console.log(text);
l = text.toLowerCase();
$(`img[data-input-id='${input.id}'`).removeClass("image-ident-loading");
console.log(l);
if (l.includes("accesion") || l.includes("number") || l.search(/(ref|rk9|rh8|ra9|rbz)\d+/g) > -1) {
console.log("SHIT", input);
$(input).addClass("image-ident-warning");
$(`img[data-input-id='${input.id}'`).addClass("image-ident-warning");
} else {
$(input).addClass("image-ident-ok");
}
await worker.terminate();
})();
}
}
</script>
{{ form.media }}
{% endblock %}
{% block content %}
<h2>Submit Question</h2>
<form action="" method="post" enctype="multipart/form-data" id="anatomyquestion-form">
{% csrf_token %}
<a href="/rapids/abnormality/create" id="add_abnormality" class="add-popup"
onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a>
<a href="/rapids/examination/create" id="add_examination" class="add-popup"
onclick="return showAddPopup(this);"><img src="{% static '/img/icon-addlink.svg' %}"></a>
<a href="/rapids/region/create" id="add_region" class="add-popup" onclick="return showAddPopup(this);"><img
src="{% static '/img/icon-addlink.svg' %}"></a>
<table>
{{ form.as_table }}
</table>
<div id="drop-container" class="drop-target">Drop image here
<div id="drop-filenames"></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 and Clone" name="submit-clone">
</form>
<div id="empty_form" style="display:none">
<ul class='no_error answer-formset'>
{{ answer_formset.empty_form.as_ul }}
</ul>
</div>
{% endblock %}
+2 -1
View File
@@ -16,7 +16,8 @@
<script src="{% static 'js/cornerstone/cornerstoneWADOImageLoader.js' %}"></script>
<script src="{% static 'js/cornerstone/cornerstone-base64-image-loader.umd.js' %}"></script>
<script src="{% static 'js/anatomy.js' %}" defer="defer"></script>
{% block js %}
{% endblock %}
</head>
<body>
+2
View File
@@ -7,6 +7,8 @@ urlpatterns = [
# path('', views.question_list, name='question_list'),
path("", views.index, name="index"),
path("question/<int:pk>/", views.question_detail, name="question_detail"),
path("question/create/", views.AnatomyQuestionCreate.as_view(), name="anatomy_question_create"),
path("question/<int:pk>/update", views.AnatomyQuestionUpdate.as_view(), name="anatomy_question_create"),
path("question/<int:pk>/save_annotation", views.question_save_annotation, name="question_save_annotation"),
path("question/<int:pk>/answer/",
views.answer_question,
+113 -2
View File
@@ -6,14 +6,22 @@ from django import forms
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic import ListView
from django.db.models.functions import Lower
from django.core.cache import cache
from django.urls import reverse_lazy, reverse
from django.http import Http404, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse
from .forms import AnatomyAnswerForm, MarkAnatomyQuestionForm
from .forms import AnatomyAnswerForm, AnswerFormSet, MarkAnatomyQuestionForm, AnatomyQuestionForm
from .models import (
AnatomyQuestion,
CidUserAnswer,
@@ -748,4 +756,107 @@ def cid_scores(request, pk):
"exams": exams,
"cid": cid,
},
)
)
class AnatomyQuestionCreateBase(LoginRequiredMixin, CreateView):
model = AnatomyQuestion
form_class = AnatomyQuestionForm
def get_context_data(self, **kwargs):
context = super(AnatomyQuestionCreateBase, self).get_context_data(**kwargs)
if self.request.POST:
context['answer_formset'] = AnswerFormSet(self.request.POST,
self.request.FILES)
else:
context['answer_formset'] = AnswerFormSet()
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']
if formset.is_valid():
response = super().form_valid(form)
formset.instance = self.object
formset.save()
# If the normal submit button is pressed we save as normal
if "submit" in self.request.POST:
return response
# else we redirect to the clone url
else:
return redirect('AnatomyQuestions:AnatomyQuestion_clone', pk=self.object.pk)
else:
return super().form_invalid(form)
#@login_required
class AnatomyQuestionCreate(AnatomyQuestionCreateBase):
#initial = {'laterality': AnatomyQuestion.NONE}
def get_initial(self):
pass
# # There has to be a better way...
# try:
# s = (i.pk for i in self.request.user.AnatomyQuestion_default.site.all())
# self.initial.update({'site': s})
# except AttributeError:
# pass
# return self.initial
#
# fields = '__all__'
# #fields = [ 'condition' ]
# #initial = {'date_of_death': '05/01/2018'}
# exclude = [ 'created_date', 'published_date' ]
#self.object = form.save(commit=False)
#self.object.save()
#form.instance.author.add(self.request.user.id)
#return super().form_valid(form)
#class AnatomyQuestionUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin,
class AnatomyQuestionUpdate(LoginRequiredMixin, UpdateView):
model = AnatomyQuestion
form_class = AnatomyQuestionForm
# fields = '__all__'
# #fields = [ 'condition' ]
# #initial = {'date_of_death': '05/01/2018'}
# exclude = [ 'created_date', 'published_date' ]
def get_context_data(self, **kwargs):
context = super(AnatomyQuestionUpdate, self).get_context_data(**kwargs)
if self.request.POST:
context['answer_formset'] = AnswerFormSet(self.request.POST,
self.request.FILES,
instance=self.object)
context['answer_formset'].full_clean()
else:
context['answer_formset'] = AnswerFormSet(instance=self.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)