start anatomy creation forms
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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)
|
||||
Reference in New Issue
Block a user