.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.3 on 2021-02-06 09:05
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('anatomy', '0027_anatomyquestion_examination'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='anatomyquestion',
|
||||
name='open_access',
|
||||
field=models.BooleanField(default=True, help_text='If a question should be freely available to browse'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='anatomyquestion',
|
||||
name='question_type',
|
||||
field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.SET_NULL, to='anatomy.questiontype'),
|
||||
),
|
||||
]
|
||||
+2
-2
@@ -68,7 +68,7 @@ class AnatomyQuestion(models.Model):
|
||||
# image = models.ImageField()
|
||||
# feedback = models.TextField(null=True)
|
||||
question_type = models.ForeignKey(
|
||||
QuestionType, on_delete=models.SET_NULL, null=True
|
||||
QuestionType, on_delete=models.SET_NULL, null=True, default=1
|
||||
)
|
||||
|
||||
image = models.ImageField(upload_to=image_directory_path, storage=image_storage)
|
||||
@@ -108,7 +108,7 @@ class AnatomyQuestion(models.Model):
|
||||
created_date = models.DateTimeField(default=timezone.now)
|
||||
|
||||
open_access = models.BooleanField(
|
||||
help_text="If an question should be freely available to browse", default=True
|
||||
help_text="If a question should be freely available to browse", default=True
|
||||
)
|
||||
|
||||
author = models.ManyToManyField(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.1.3 on 2021-02-06 09:05
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('longs', '0009_auto_20210205_1146'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='longseries',
|
||||
name='author',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='long_series', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='long',
|
||||
name='open_access',
|
||||
field=models.BooleanField(default=True, help_text='If a question should be freely available to browse'),
|
||||
),
|
||||
]
|
||||
+114
-64
@@ -22,9 +22,11 @@ import string
|
||||
from anatomy.models import Modality
|
||||
|
||||
from generic.models import Examination, Condition, Sign
|
||||
#from generic.models import Examination, Site, Condition, Sign
|
||||
|
||||
# from generic.models import Examination, Site, Condition, Sign
|
||||
|
||||
from easy_thumbnails.files import get_thumbnailer
|
||||
from easy_thumbnails.exceptions import InvalidImageFormatError
|
||||
|
||||
image_storage = FileSystemStorage(
|
||||
# Physical file location ROOT
|
||||
@@ -33,21 +35,23 @@ image_storage = FileSystemStorage(
|
||||
base_url=u"{0}longs/".format(settings.MEDIA_URL),
|
||||
)
|
||||
|
||||
|
||||
def image_directory_path(instance, filename):
|
||||
return u"picture/{0}".format(filename)
|
||||
|
||||
|
||||
def findMiddle(input_list):
|
||||
middle = float(len(input_list))/2
|
||||
middle = float(len(input_list)) / 2
|
||||
if middle % 2 != 0:
|
||||
return input_list[int(middle - .5)]
|
||||
return input_list[int(middle - 0.5)]
|
||||
else:
|
||||
return (input_list[int(middle)])
|
||||
return (input_list[int(middle)], input_list[int(middle-1)])
|
||||
return input_list[int(middle)]
|
||||
return (input_list[int(middle)], input_list[int(middle - 1)])
|
||||
|
||||
|
||||
class Long(models.Model):
|
||||
#author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
||||
#image = models.ImageField()
|
||||
# author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
||||
# image = models.ImageField()
|
||||
history = models.TextField(null=True, blank=True)
|
||||
|
||||
feedback = models.TextField(null=True, blank=True)
|
||||
@@ -55,12 +59,12 @@ class Long(models.Model):
|
||||
condition = tagulous.models.TagField(
|
||||
to=Condition,
|
||||
blank=True,
|
||||
help_text=
|
||||
"Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes \"...\""
|
||||
help_text='Associated condition. Will allow searching / filtering and tips / hints to be displayed. Conditions with spaces must be enclosed in quotes "..."',
|
||||
)
|
||||
|
||||
sign = tagulous.models.TagField(
|
||||
to=Sign, blank=True, help_text='Radiological signs in the question')
|
||||
to=Sign, blank=True, help_text="Radiological signs in the question"
|
||||
)
|
||||
|
||||
# Model answers
|
||||
model_observations = models.TextField(null=True, blank=True)
|
||||
@@ -69,9 +73,8 @@ class Long(models.Model):
|
||||
model_differential_diagnosis = models.TextField(null=True, blank=True)
|
||||
model_management = models.TextField(null=True, blank=True)
|
||||
|
||||
|
||||
DEFAULT_SITE_ID = 1
|
||||
#site = models.ManyToManyField(
|
||||
# site = models.ManyToManyField(
|
||||
# Site,
|
||||
# blank=True,
|
||||
# default=DEFAULT_SITE_ID,
|
||||
@@ -80,22 +83,23 @@ class Long(models.Model):
|
||||
verified = models.BooleanField(default=False)
|
||||
created_date = models.DateTimeField(default=timezone.now)
|
||||
published_date = models.DateTimeField(blank=True, null=True)
|
||||
author = models.ManyToManyField(settings.AUTH_USER_MODEL,
|
||||
blank=True,
|
||||
help_text='Author of question',
|
||||
related_name="long_authored_questions")
|
||||
|
||||
scrapped = models.BooleanField(
|
||||
default=False,
|
||||
help_text='Question has been scrapped and will not be shown')
|
||||
|
||||
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 of question",
|
||||
related_name="long_authored_questions",
|
||||
)
|
||||
|
||||
scrapped = models.BooleanField(
|
||||
default=False, help_text="Question has been scrapped and will not be shown"
|
||||
)
|
||||
|
||||
open_access = models.BooleanField(
|
||||
help_text="If a question should be freely available to browse", default=True
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('longs:long_detail', kwargs={'pk': self.pk})
|
||||
return reverse("longs:long_detail", kwargs={"pk": self.pk})
|
||||
|
||||
def get_authors(self):
|
||||
"""Returns a comma seperated text list of authors"""
|
||||
@@ -116,7 +120,6 @@ class Long(models.Model):
|
||||
exams = ", ".join(e)
|
||||
return exams
|
||||
|
||||
|
||||
def GetImages(self, feedback=False):
|
||||
qs = self.images.all()
|
||||
|
||||
@@ -128,28 +131,64 @@ class Long(models.Model):
|
||||
return images
|
||||
|
||||
def GetImageUrls(self):
|
||||
return ",".join(["http://penracourses.org.uk{}".format(i.url) for i in self.GetImages()])
|
||||
#def GetNonFeedbackQuestionImages(self):
|
||||
#return self.GetImages()
|
||||
return ",".join(
|
||||
["http://penracourses.org.uk{}".format(i.url) for i in self.GetImages()]
|
||||
)
|
||||
|
||||
# def GetNonFeedbackQuestionImages(self):
|
||||
# return self.GetImages()
|
||||
|
||||
|
||||
class LongSeriesImage(models.Model):
|
||||
image = models.ImageField(upload_to=image_directory_path, storage=image_storage)
|
||||
position = models.IntegerField(default=0)
|
||||
series = models.ForeignKey("LongSeries", related_name="images", on_delete=models.SET_NULL, null=True)
|
||||
series = models.ForeignKey(
|
||||
"LongSeries", related_name="images", on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
|
||||
|
||||
class LongSeries(models.Model):
|
||||
modality = models.ForeignKey(Modality, related_name="series_modality", on_delete=models.SET_NULL, null=True)
|
||||
modality = models.ForeignKey(
|
||||
Modality, related_name="series_modality", on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
examination = models.ForeignKey(
|
||||
Examination, help_text="Name of the examination", related_name="series_examination", on_delete=models.SET_NULL, null=True)
|
||||
long = models.ForeignKey("Long", help_text="The question this series should be associated with", related_name="series", on_delete=models.SET_NULL, null=True)
|
||||
description = models.TextField(blank=True, help_text="Description of stack, for admin organisation, will not be visible when taking")
|
||||
Examination,
|
||||
help_text="Name of the examination",
|
||||
related_name="series_examination",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
)
|
||||
long = models.ForeignKey(
|
||||
"Long",
|
||||
help_text="The question this series should be associated with",
|
||||
related_name="series",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
)
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
help_text="Description of stack, for admin organisation, will not be visible when taking",
|
||||
)
|
||||
|
||||
author = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name="long_series",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "{} : {} [{}]".format(self.get_examination(), self.description, self.long.pk)
|
||||
return "{} : {} [{}]".format(
|
||||
self.get_examination(), self.description, self.long.pk
|
||||
)
|
||||
|
||||
def get_author_objects(self):
|
||||
"""Returns a comma seperated text list of authors"""
|
||||
if self.author:
|
||||
return self.author.all()
|
||||
else:
|
||||
return ["None"]
|
||||
if self.long:
|
||||
authors = [i for i in self.long.author.all()]
|
||||
else:
|
||||
@@ -157,15 +196,21 @@ class LongSeries(models.Model):
|
||||
|
||||
return authors
|
||||
|
||||
def get_author_display(self):
|
||||
return ", ".join(self.get_author_objects())
|
||||
|
||||
def get_examination(self):
|
||||
"""Returns a comma seperated text list of regions"""
|
||||
return str(self.examination)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('longs:long_series_detail', kwargs={'pk': self.pk})
|
||||
return reverse("longs:long_series_detail", kwargs={"pk": self.pk})
|
||||
|
||||
def get_image_urls(self, feedback=False):
|
||||
images = ["http://penracourses.org.uk{}".format(i.image.url) for i in self.images.all()]
|
||||
images = [
|
||||
"http://penracourses.org.uk{}".format(i.image.url)
|
||||
for i in self.images.all()
|
||||
]
|
||||
|
||||
return ",".join(images)
|
||||
|
||||
@@ -173,21 +218,25 @@ class LongSeries(models.Model):
|
||||
images = self.images.all()
|
||||
|
||||
if len(images) < 1:
|
||||
return 'No images', 0
|
||||
return "No images", 0
|
||||
|
||||
img = findMiddle(images).image
|
||||
thumbnailer = get_thumbnailer(img)
|
||||
try:
|
||||
thumbnailer = get_thumbnailer(img)
|
||||
except InvalidImageFormatError:
|
||||
return format_html('<span title="{}">Invalid image url<span>', image_object)
|
||||
thumbnail = thumbnailer["exam-list"]
|
||||
return '<img src="/media/{}" />'.format(thumbnail), len(images)
|
||||
|
||||
|
||||
|
||||
def get_block(self):
|
||||
examination = self.get_examination()
|
||||
thumb, image_number = self.get_thumbnail()
|
||||
return format_html('<div>{}<br/>{}<br/>Images: {}</div>', examination, thumb, image_number )
|
||||
return format_html(
|
||||
"<div>{}<br/>{}<br/>Images: {}</div>", examination, thumb, image_number
|
||||
)
|
||||
|
||||
#class LongImage(models.Model):
|
||||
|
||||
# class LongImage(models.Model):
|
||||
# long = models.ForeignKey(Long,
|
||||
# related_name="images",
|
||||
# on_delete=models.CASCADE)
|
||||
@@ -207,47 +256,43 @@ class LongSeries(models.Model):
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
long = models.ForeignKey(Long,
|
||||
related_name="long_notes",
|
||||
on_delete=models.CASCADE,
|
||||
null=True)
|
||||
#author = models.ForeignKey(User,
|
||||
author = models.ForeignKey( settings.AUTH_USER_MODEL,
|
||||
related_name="long_notes",
|
||||
on_delete=models.CASCADE)
|
||||
long = models.ForeignKey(
|
||||
Long, related_name="long_notes", on_delete=models.CASCADE, null=True
|
||||
)
|
||||
# author = models.ForeignKey(User,
|
||||
author = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, related_name="long_notes", on_delete=models.CASCADE
|
||||
)
|
||||
note = models.TextField()
|
||||
created_on = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def get_absolute_url(self):
|
||||
self.pk = self.long_id
|
||||
return reverse('longs:long_detail', kwargs={'pk': self.pk})
|
||||
return reverse("longs:long_detail", kwargs={"pk": self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return "{} [{}] {}".format(self.long, self.author, self.created_on)
|
||||
|
||||
|
||||
class LongCreationDefault(models.Model):
|
||||
#author = models.OneToOneField(User,
|
||||
# author = models.OneToOneField(User,
|
||||
author = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
related_name="long_default",
|
||||
on_delete=models.CASCADE)
|
||||
settings.AUTH_USER_MODEL, related_name="long_default", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
#site = models.ManyToManyField(
|
||||
# site = models.ManyToManyField(
|
||||
# Site,
|
||||
# blank=True,
|
||||
# default=1,
|
||||
# help_text="Default site to use when creating a new long")
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('longs:long_create')
|
||||
return reverse("longs:long_create")
|
||||
|
||||
|
||||
class Exam(models.Model):
|
||||
name = models.CharField(max_length=200)
|
||||
exam_questions = SortedManyToManyField(
|
||||
Long, related_name="exams", blank="true"
|
||||
)
|
||||
exam_questions = SortedManyToManyField(Long, related_name="exams", blank="true")
|
||||
|
||||
active = models.BooleanField(
|
||||
help_text="If an exam should be available", default=True
|
||||
@@ -262,7 +307,8 @@ class Exam(models.Model):
|
||||
)
|
||||
|
||||
time_limit = models.IntegerField(
|
||||
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 __str__(self):
|
||||
@@ -280,6 +326,7 @@ class Exam(models.Model):
|
||||
def get_question_index(self, question):
|
||||
return list(self.exam_questions.all()).index(question)
|
||||
|
||||
|
||||
class CidUserAnswer(models.Model):
|
||||
"""User answers by candidate"""
|
||||
|
||||
@@ -287,7 +334,6 @@ class CidUserAnswer(models.Model):
|
||||
Long, related_name="cid_user_answers", on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
|
||||
# Answers
|
||||
answer_observations = models.TextField(null=True, blank=True)
|
||||
answer_interpretation = models.TextField(null=True, blank=True)
|
||||
@@ -295,7 +341,11 @@ class CidUserAnswer(models.Model):
|
||||
answer_differential_diagnosis = models.TextField(null=True, blank=True)
|
||||
answer_management = models.TextField(null=True, blank=True)
|
||||
|
||||
cid = models.BigIntegerField(blank=True, null=True, help_text="Candidate ID (limitied by BigIntegerField size)")
|
||||
cid = models.BigIntegerField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Candidate ID (limitied by BigIntegerField size)",
|
||||
)
|
||||
|
||||
# Each user answer is associated with a particular exam
|
||||
exam = models.ForeignKey(
|
||||
@@ -323,7 +373,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))
|
||||
|
||||
self.answer_compare = s
|
||||
|
||||
|
||||
+10
-8
@@ -17,12 +17,14 @@ class ImageColumn(tables.Column):
|
||||
if obj is None:
|
||||
return format_html('<span>No image<span>')
|
||||
|
||||
image_object = obj.image
|
||||
try:
|
||||
thumbnailer = get_thumbnailer(image_object)
|
||||
return format_html('<img src="/media/{}" />', thumbnailer["exam-list"])
|
||||
except InvalidImageFormatError:
|
||||
return format_html('<span title="{}">Invalid image url<span>', image_object)
|
||||
return obj.get_thumbnail()[0]
|
||||
|
||||
#image_object = obj.image
|
||||
#try:
|
||||
# thumbnailer = get_thumbnailer(image_object)
|
||||
# return format_html('<img src="/media/{}" />', thumbnailer["exam-list"])
|
||||
#except InvalidImageFormatError:
|
||||
# return format_html('<span title="{}">Invalid image url<span>', image_object)
|
||||
|
||||
|
||||
class LongTable(tables.Table):
|
||||
@@ -38,11 +40,11 @@ class LongTable(tables.Table):
|
||||
text='Clone',
|
||||
args=[A('pk')],
|
||||
orderable=False)
|
||||
images = ImageColumn("images", orderable=False)
|
||||
series = ImageColumn("images", orderable=False)
|
||||
|
||||
class Meta:
|
||||
model = Long
|
||||
template_name = "django_tables2/bootstrap4.html"
|
||||
fields = ("normal", "abnormality", "region", "examination",
|
||||
"laterality", "site", "created_date", "author")
|
||||
sequence = ("view", "images")
|
||||
sequence = ("view", "series")
|
||||
@@ -19,7 +19,7 @@ Longs:
|
||||
{% if request.user.is_authenticated %}
|
||||
<a href="{% url 'longs:exam_list' %}">Exams</a> /
|
||||
<a href="{% url 'longs:long_view' %}">Questions</a> /
|
||||
<a href="{% url 'longs:long_create' %}" title="Create a new question">Create Question</a>
|
||||
<a href="{% url 'longs:long_create' %}" title="Create a new long case">Create Case</a> /
|
||||
<a href="{% url 'longs:long_series_create' %}" title="Create a new image series">Create Series</a>
|
||||
{% endif %}
|
||||
{% comment %} </br>
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
>Popup</a>
|
||||
</span>
|
||||
{% endfor %}
|
||||
<a href="{% url 'longs:long_series_id_create' pk=question.pk %}">Add series</a>
|
||||
<span><a href="{% url 'longs:long_series_id_create' pk=question.pk %}">Add new series</a><br/>
|
||||
<a href="{% url 'longs:long_series_id_create' pk=question.pk %}">Add existing series</a></span>
|
||||
</div>
|
||||
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
||||
<p><b>Author(s):</b> {% for author in question.author.all %} <a
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Submit Long Case</h2>
|
||||
Use this form to create a long case. Once the case has been created it can be associated with image sets (series)
|
||||
Use this form to create a long case. Associated image sets can be added using this form however the images will have to be uploaded once the case has been created.
|
||||
<form action="" method="post" enctype="multipart/form-data" id="long-form">
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
<div>{{ series.modality}}, {{ series.examination }}</div>
|
||||
|
||||
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ series.get_image_urls }}" data-annotations=''></div>
|
||||
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ series.get_image_urls }}" data-annotations=''></div>
|
||||
|
||||
<div>Author: {{ series.get_author_display }}</div>
|
||||
+3
-1
@@ -294,7 +294,7 @@ class LongSeriesCreate(LoginRequiredMixin, CreateView):
|
||||
self.object = form.save(commit=False)
|
||||
self.object.save()
|
||||
|
||||
#form.instance.author.add(self.request.user.id)
|
||||
form.instance.author.add(self.request.user.id)
|
||||
|
||||
context = self.get_context_data(form=form)
|
||||
series_formset = context["image_formset"]
|
||||
@@ -336,6 +336,8 @@ class LongSeriesUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, UpdateV
|
||||
#self.object.exams.clear()
|
||||
#self.object.exams.add(*new_exams)
|
||||
|
||||
form.instance.author.add(self.request.user.id)
|
||||
|
||||
context = self.get_context_data(form=form)
|
||||
image_formset = context["image_formset"]
|
||||
# logger.debug(formset.is_valid())
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2021-02-06 09:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rapids', '0011_auto_20210205_1056'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='rapid',
|
||||
name='open_access',
|
||||
field=models.BooleanField(default=True, help_text='If a question should be freely available to browse'),
|
||||
),
|
||||
]
|
||||
+1
-1
@@ -168,7 +168,7 @@ class Rapid(models.Model):
|
||||
help_text='Question has been scrapped and will not be shown')
|
||||
|
||||
open_access = models.BooleanField(
|
||||
help_text="If an question should be freely available to browse", default=True
|
||||
help_text="If a question should be freely available to browse", default=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user