Merge branch 'master' of gitssh://web.xkjq.uk:30001/ross/rad

This commit is contained in:
Ross
2021-02-13 20:19:19 +00:00
25 changed files with 364 additions and 186 deletions
+17 -1
View File
@@ -1,6 +1,22 @@
import django_filters import django_filters
from .models import AnatomyQuestion from .models import AnatomyQuestion, CidUserAnswer
#from generic.filters import UserAnswerFilter
class AnatomyUserAnswerFilter(django_filters.FilterSet):
class Meta:
model= CidUserAnswer
fields = (
"cid",
"question",
"answer",
#"answer_compare",
"exam",
"created",
"updated",
)
class AnatomyQuestionFilter(django_filters.FilterSet): class AnatomyQuestionFilter(django_filters.FilterSet):
@@ -0,0 +1,20 @@
# Generated by Django 3.1.3 on 2021-02-09 22:39
import anatomy.models
import django.core.files.storage
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('anatomy', '0028_auto_20210206_0905'),
]
operations = [
migrations.AlterField(
model_name='anatomyquestion',
name='image',
field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/anatomy/', location='/home/ross/rad/media//anatomy/'), upload_to=anatomy.models.image_directory_path),
),
]
-35
View File
@@ -310,41 +310,6 @@ class Exam(ExamBase):
return exam_json return exam_json
# class UserAnswer(models.Model):
# question = models.ForeignKey(
# AnatomyQuestion, related_name="user_answers", on_delete=models.CASCADE
# )
# answer = models.TextField(max_length=500, blank=True)
# user = models.ForeignKey(
# settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL
# )
# 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(
# Exam, related_name="exam", on_delete=models.CASCADE, null=True
# )
# flagged = models.BooleanField(default=False)
# created = models.DateTimeField(auto_now_add=True)
# updated = models.DateTimeField(auto_now=True)
# def __str__(self):
# try:
# exam = self.exam
# except (Exam.DoesNotExist, KeyError) as e:
# exam = "None"
# return "{}/{}/{}: {}".format(
# exam, self.cid, self.question.GetPrimaryAnswer(), self.answer
# )
# def clean(self):
# if self.answer:
# self.answer = self.answer.strip()
class CidUserAnswer(models.Model): class CidUserAnswer(models.Model):
"""User answers by candidate""" """User answers by candidate"""
+22 -1
View File
@@ -1,7 +1,7 @@
import django_tables2 as tables import django_tables2 as tables
from django_tables2.utils import A from django_tables2.utils import A
from .models import AnatomyQuestion from .models import AnatomyQuestion, CidUserAnswer
from django.utils.html import format_html from django.utils.html import format_html
@@ -9,6 +9,27 @@ from easy_thumbnails.files import get_thumbnailer
from easy_thumbnails.exceptions import InvalidImageFormatError from easy_thumbnails.exceptions import InvalidImageFormatError
#from generic.tables import UserAnswerTable
class AnatomyUserAnswerTable(tables.Table):
select = tables.CheckBoxColumn(accessor=("pk"))
delete = tables.LinkColumn(
"anatomy:anatomy_user_answer_delete", text="Delete", args=[A("pk")], orderable=False
)
class Meta:
model = CidUserAnswer
template_name = "django_tables2/bootstrap4.html"
fields = (
"cid",
"question",
"answer",
#"answer_compare",
"exam",
"created",
"updated",
)
class ImageColumn(tables.Column): class ImageColumn(tables.Column):
def render(self, value): def render(self, value):
@@ -0,0 +1,4 @@
<form method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
<input type="submit" value="Confirm">
</form>
+2
View File
@@ -51,4 +51,6 @@ urlpatterns = [
path("examination/ajax/get_examination_id", path("examination/ajax/get_examination_id",
views.get_examination_id, views.get_examination_id,
name="get_examination_id"), name="get_examination_id"),
path("user_answers/", views.AnatomyUserAnswerView.as_view(), name="anatomy_user_answer_view"),
path("user_answers/<int:pk>/delete", views.UserAnswerDelete.as_view(), name="anatomy_user_answer_delete"),
] ]
+15 -2
View File
@@ -46,8 +46,8 @@ from .models import (
from generic.models import Examination from generic.models import Examination
from generic.views import ExamViews from generic.views import ExamViews
from .tables import AnatomyQuestionTable from .tables import AnatomyQuestionTable, AnatomyUserAnswerTable
from .filters import AnatomyQuestionFilter from .filters import AnatomyQuestionFilter, AnatomyUserAnswerFilter
from django_tables2 import SingleTableView, SingleTableMixin from django_tables2 import SingleTableView, SingleTableMixin
from django_filters.views import FilterView from django_filters.views import FilterView
@@ -62,6 +62,8 @@ from helpers.images import image_as_base64
from django.template.defaulttags import register from django.template.defaulttags import register
from generic.mixins import SuperuserRequiredMixin
@register.filter @register.filter
def get_item(dictionary, key): def get_item(dictionary, key):
@@ -919,6 +921,13 @@ class AnatomyQuestionView(SingleTableMixin, FilterView):
filterset_class = AnatomyQuestionFilter filterset_class = AnatomyQuestionFilter
class AnatomyUserAnswerView(SingleTableMixin, FilterView):
model = CidUserAnswer
table_class = AnatomyUserAnswerTable
template_name = "anatomy/anatomy_question_view.html"
filterset_class = AnatomyUserAnswerFilter
def question_save_annotation(request, pk): def question_save_annotation(request, pk):
if request.is_ajax() and request.method == "POST": if request.is_ajax() and request.method == "POST":
@@ -936,3 +945,7 @@ def question_save_annotation(request, pk):
AnatomyExamViews = ExamViews(Exam, "anatomy", "anatomy", loadJsonAnswer) AnatomyExamViews = ExamViews(Exam, "anatomy", "anatomy", loadJsonAnswer)
class UserAnswerDelete(SuperuserRequiredMixin, DeleteView):
model = CidUserAnswer
success_url = reverse_lazy("anatomy:anatomy_user_answer_view")
+18
View File
@@ -0,0 +1,18 @@
import django_filters
#class UserAnswerFilter(django_filters.FilterSet):
# class Meta:
# #model = AnatomyQuestion
# abstract = True
# fields = (
# "cid",
# "question",
# "answer",
# #"answer_compare",
# "exam",
# "created",
# "updateded",
# "created_date",
# )
+6
View File
@@ -0,0 +1,6 @@
from django.contrib.auth.mixins import UserPassesTestMixin
class SuperuserRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_superuser
+47
View File
@@ -0,0 +1,47 @@
import django_tables2 as tables
from django_tables2.utils import A
from django.utils.html import format_html
from easy_thumbnails.files import get_thumbnailer
from easy_thumbnails.exceptions import InvalidImageFormatError
class ImageColumn(tables.Column):
def render(self, value):
try:
thumbnailer = get_thumbnailer(value)
return format_html('<img src="/media/{}" />', thumbnailer["exam-list"])
except InvalidImageFormatError:
return format_html('<span title="{}">Invalid image url<span>', value)
#class UserAnswerTable(tables.Table):
# #edit = tables.LinkColumn(
# # "anatomy:anatomy_question_update", text="Edit", args=[A("pk")], orderable=False
# #)
# #view = tables.LinkColumn(
# # "anatomy:question_detail", text="View", args=[A("pk")], orderable=False
# #)
# #image = ImageColumn("image", orderable=False)
# # clone = tables.LinkColumn('anatomy:anatomy_question_clone',
# # text='Clone',
# # args=[A('pk')],
# # orderable=False)
# #exams = tables.ManyToManyColumn(verbose_name="Exams")
#
# class Meta:
# #model = AnatomyQuestion
# abstract = True
# template_name = "django_tables2/bootstrap4.html"
# fields = (
# "cid",
# "question",
# "answer",
# #"answer_compare",
# "exam",
# "created",
# "updated",
# )
# #sequence = ("view")
+1 -1
View File
@@ -210,7 +210,7 @@ class ExamViews(View, LoginRequiredMixin):
return JsonResponse(active_exams) return JsonResponse(active_exams)
@method_decorator(csrf_exempt) @method_decorator(csrf_exempt)
def postExamAnswers(request): def postExamAnswers(self, request):
if request.is_ajax and request.method == "POST": if request.is_ajax and request.method == "POST":
n = 0 n = 0
+2
View File
@@ -21,6 +21,8 @@ def image_as_base64(image_file):
# encoded_string = base64.b64encode(img_f.read()) # encoded_string = base64.b64encode(img_f.read())
encoded_string = base64.b64encode(image_file.file.read()) encoded_string = base64.b64encode(image_file.file.read())
mimetype, enc = mimetypes.guess_type(image_file.path) mimetype, enc = mimetypes.guess_type(image_file.path)
if "dicom" in mimetype:
return "data:{};base64,{}".format(mimetype, encoded_string.decode("utf-8"))
return "data:image/{};base64,{}".format(mimetype, encoded_string.decode("utf-8")) return "data:image/{};base64,{}".format(mimetype, encoded_string.decode("utf-8"))
@@ -0,0 +1,20 @@
# Generated by Django 3.1.3 on 2021-02-11 18:27
import django.core.files.storage
from django.db import migrations, models
import longs.models
class Migration(migrations.Migration):
dependencies = [
('longs', '0013_auto_20210206_2147'),
]
operations = [
migrations.AlterField(
model_name='longseriesimage',
name='image',
field=models.FileField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/longs/', location='/home/ross/rad/media/longs/'), upload_to=longs.models.image_directory_path),
),
]
+61 -3
View File
@@ -15,9 +15,13 @@ from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.html import mark_safe from django.utils.html import mark_safe
from django.core.exceptions import ValidationError
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
import string import string
from collections import defaultdict
from helpers.images import image_as_base64
from anatomy.models import Modality from anatomy.models import Modality
@@ -30,7 +34,7 @@ from easy_thumbnails.exceptions import InvalidImageFormatError
image_storage = FileSystemStorage( image_storage = FileSystemStorage(
# Physical file location ROOT # Physical file location ROOT
location=u"{0}/longs/".format(settings.MEDIA_ROOT), location=u"{0}longs/".format(settings.MEDIA_ROOT),
# Url for file # Url for file
base_url=u"{0}longs/".format(settings.MEDIA_URL), base_url=u"{0}longs/".format(settings.MEDIA_URL),
) )
@@ -139,8 +143,11 @@ class Long(models.Model):
# return self.GetImages() # return self.GetImages()
def test_image_validator(file):
pass
class LongSeriesImage(models.Model): class LongSeriesImage(models.Model):
image = models.ImageField(upload_to=image_directory_path, storage=image_storage) image = models.FileField(upload_to=image_directory_path, storage=image_storage)
position = models.IntegerField(default=0) position = models.IntegerField(default=0)
series = models.ForeignKey( series = models.ForeignKey(
"LongSeries", related_name="images", on_delete=models.SET_NULL, null=True "LongSeries", related_name="images", on_delete=models.SET_NULL, null=True
@@ -200,7 +207,7 @@ class LongSeries(models.Model):
return authors return authors
def get_author_display(self): def get_author_display(self):
return ", ".join(self.get_author_objects()) return ", ".join([i.username for i in self.get_author_objects()])
def get_examination(self): def get_examination(self):
"""Returns a comma seperated text list of regions""" """Returns a comma seperated text list of regions"""
@@ -303,6 +310,57 @@ class Exam(ExamBase):
default=4500, default=4500,
) )
def get_exam_json(self):
questions = self.exam_questions.all()
exam_questions = defaultdict(dict)
exam_order = []
for q in questions:
exam_order.append(q.id)
# Loop through longimage associations
images = []
image_titles = []
for series in q.series.all():
image_array = []
for i in series.images.all():
image_array.append(image_as_base64(i.image))
#image_array.append(i.image.url)
images.append(image_array)
image_titles.append(series.get_examination())
exam_questions[q.id] = {
"title": q.history,
"images": images,
"image_titles": image_titles,
#"feedback_image": [],
#"annotations": [str(q.image_annotations)],
"type": "long",
}
#if feedback_images:
# exam_questions[q.id]["feedback_image"] = feedback_images
exam_json = {
"eid": "long/{}".format(self.id),
"cached": False,
"exam_type": "long",
"exam_name": self.name,
"exam_mode": True,
"exam_order": exam_order,
"questions": exam_questions,
}
if self.time_limit:
exam_json["exam_time"] = self.time_limit
return exam_json
class CidUserAnswer(models.Model): class CidUserAnswer(models.Model):
"""User answers by candidate""" """User answers by candidate"""
+3
View File
@@ -31,6 +31,9 @@ class LongSeriesImageColumn(tables.Column):
def render(self, value): def render(self, value):
obj = value.first() obj = value.first()
if obj is None:
return "None"
image_object = obj.image image_object = obj.image
try: try:
thumbnailer = get_thumbnailer(image_object) thumbnailer = get_thumbnailer(image_object)
+1 -1
View File
@@ -48,7 +48,7 @@ urlpatterns = [
path("exam/submit", views.LongExamViews.postExamAnswers, name="exam_answers_submit"), path("exam/submit", views.LongExamViews.postExamAnswers, name="exam_answers_submit"),
path("exam/", views.LongExamViews.exam_list, name="exam_list"), path("exam/", views.LongExamViews.exam_list, name="exam_list"),
path("exam/json/", views.LongExamViews.active_exams, name="active_exams"), path("exam/json/", views.LongExamViews.active_exams, name="active_exams"),
path("exam/json/<int:pk>", views.exam_json, name="exam_json"), path("exam/json/<int:pk>", views.LongExamViews.exam_json, name="exam_json"),
path( path(
"exam/json/<int:pk>/recreate", "exam/json/<int:pk>/recreate",
views.LongExamViews.exam_json_recreate, views.LongExamViews.exam_json_recreate,
-69
View File
@@ -796,75 +796,6 @@ def mark(request, pk, sk):
) )
def exam_json(request, pk):
exam = get_object_or_404(Exam, pk=pk)
if not exam.active:
raise Http404("No available exam")
exam_json_cache = cache.get("longs_exam_json_{}".format(pk))
if exam_json_cache is not None and not exam.recreate_json:
exam_json_cache["cached"] = True
return JsonResponse(exam_json_cache)
questions = exam.exam_questions.all()
exam_questions = defaultdict(dict)
exam_order = []
for q in questions:
exam_order.append(q.id)
# Loop through longimage associations
images = []
feedback_images = []
for i in q.images.all():
if i.feedback_image == True:
feedback_images.append(image_as_base64(i.image))
# feedback_images.append(i.image.url)
else:
images.append(image_as_base64(i.image))
#images.append(i.image.url)
exam_questions[q.id] = {
"images": images,
#"feedback_image": [],
#"annotations": [str(q.image_annotations)],
"type": "long",
}
#if feedback_images:
# exam_questions[q.id]["feedback_image"] = feedback_images
exam_json = {
"eid": "long/{}".format(exam.id),
"cached": False,
"exam_type": "long",
"exam_name": exam.name,
"exam_mode": True,
"exam_order": exam_order,
"questions": exam_questions,
}
if exam.time_limit:
exam_json["exam_time"] = exam.time_limit
exam.recreate_json = False
exam.save()
cache.set("longs_exam_json_{}".format(pk), exam_json, 3600)
return JsonResponse(exam_json)
@login_required @login_required
def exam_scores_cid(request, pk): def exam_scores_cid(request, pk):
exam = get_object_or_404(Exam, pk=pk) exam = get_object_or_404(Exam, pk=pk)
+7
View File
@@ -50,6 +50,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'debug_toolbar', 'debug_toolbar',
"tagulous", "tagulous",
"dbbackup",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -199,8 +200,14 @@ THUMBNAIL_ALIASES = {
} }
THUMBNAIL_SOURCE_GENERATORS = ('easy_thumbnails.source_generators.pil_image', 'helpers.images.pil_dicom_image') THUMBNAIL_SOURCE_GENERATORS = ('easy_thumbnails.source_generators.pil_image', 'helpers.images.pil_dicom_image')
DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage'
DBBACKUP_STORAGE_OPTIONS = {'location': os.path.join(BASE_DIR, '../backups')}
try: try:
from .settings_local import * from .settings_local import *
except ImportError: except ImportError:
pass pass
<<<<<<< HEAD
=======
>>>>>>> 5763aae287cbceee318f975495f7d9429517c88f
@@ -0,0 +1,20 @@
# Generated by Django 3.1.3 on 2021-02-09 22:39
import django.core.files.storage
from django.db import migrations, models
import rapids.models
class Migration(migrations.Migration):
dependencies = [
('rapids', '0012_auto_20210206_0905'),
]
operations = [
migrations.AlterField(
model_name='rapidimage',
name='image',
field=models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/rapids/', location='/home/ross/rad/media//rapids/'), upload_to=rapids.models.image_directory_path),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 3.1.3 on 2021-02-11 20:34
import django.core.files.storage
from django.db import migrations, models
import rapids.models
class Migration(migrations.Migration):
dependencies = [
('rapids', '0013_auto_20210209_2239'),
]
operations = [
migrations.AlterField(
model_name='rapidimage',
name='image',
field=models.FileField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/rapids/', location='/home/ross/rad/media//rapids/'), upload_to=rapids.models.image_directory_path),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 3.1.3 on 2021-02-11 20:35
import django.core.files.storage
from django.db import migrations, models
import rapids.models
class Migration(migrations.Migration):
dependencies = [
('rapids', '0014_auto_20210211_2034'),
]
operations = [
migrations.AlterField(
model_name='rapidimage',
name='image',
field=models.FileField(storage=django.core.files.storage.FileSystemStorage(base_url='/media/rapids/', location='/home/ross/rad/media/rapids/'), upload_to=rapids.models.image_directory_path),
),
]
+53 -2
View File
@@ -16,13 +16,15 @@ from django.utils.html import mark_safe
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
import string import string
from collections import defaultdict
from helpers.images import image_as_base64
from generic.models import Site, Condition, Sign, ExamBase from generic.models import Site, Condition, Sign, ExamBase
image_storage = FileSystemStorage( image_storage = FileSystemStorage(
# Physical file location ROOT # Physical file location ROOT
location=u"{0}/rapids/".format(settings.MEDIA_ROOT), location=u"{0}rapids/".format(settings.MEDIA_ROOT),
# Url for file # Url for file
base_url=u"{0}rapids/".format(settings.MEDIA_URL), base_url=u"{0}rapids/".format(settings.MEDIA_URL),
) )
@@ -286,7 +288,7 @@ class RapidImage(models.Model):
rapid = models.ForeignKey(Rapid, rapid = models.ForeignKey(Rapid,
related_name="images", related_name="images",
on_delete=models.CASCADE) on_delete=models.CASCADE)
image = models.ImageField(upload_to=image_directory_path, storage=image_storage) image = models.FileField(upload_to=image_directory_path, storage=image_storage)
feedback_image = models.BooleanField(default=False) feedback_image = models.BooleanField(default=False)
@@ -350,6 +352,55 @@ class Exam(ExamBase):
help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", default=2100 help_text="Exam time limit (in seconds). Default is 2100 secondse (35 minutes)", default=2100
) )
def get_exam_json(self):
questions = self.exam_questions.all()
exam_questions = defaultdict(dict)
exam_order = []
for q in questions:
exam_order.append(q.id)
# Loop through rapidimage associations
images = []
feedback_images = []
for i in q.images.all():
if i.feedback_image == True:
feedback_images.append(image_as_base64(i.image))
# feedback_images.append(i.image.url)
else:
images.append(image_as_base64(i.image))
#images.append(i.image.url)
exam_questions[q.id] = {
"images": images,
#"feedback_image": [],
#"annotations": [str(q.image_annotations)],
"type": "rapid",
}
#if feedback_images:
# exam_questions[q.id]["feedback_image"] = feedback_images
exam_json = {
"eid": "rapid/{}".format(self.id),
"cached": False,
"exam_type": "rapid",
"exam_name": self.name,
"exam_mode": True,
"exam_order": exam_order,
"questions": exam_questions,
}
if self.time_limit:
exam_json["exam_time"] = self.time_limit
return exam_json
class CidUserAnswer(models.Model): class CidUserAnswer(models.Model):
"""User answers by candidate""" """User answers by candidate"""
+1 -1
View File
@@ -32,7 +32,7 @@ urlpatterns = [
path("exam/submit", views.RapidExamViews.postExamAnswers, name="exam_answers_submit"), path("exam/submit", views.RapidExamViews.postExamAnswers, name="exam_answers_submit"),
path("exam/", views.RapidExamViews.exam_list, name="exam_list"), path("exam/", views.RapidExamViews.exam_list, name="exam_list"),
path("exam/json/", views.RapidExamViews.active_exams, name="active_exams"), path("exam/json/", views.RapidExamViews.active_exams, name="active_exams"),
path("exam/json/<int:pk>", views.exam_json, name="exam_json"), path("exam/json/<int:pk>", views.RapidExamViews.exam_json, name="exam_json"),
path("exam/json/<int:pk>/recreate", views.RapidExamViews.exam_json_recreate, name="exam_json_recreate"), path("exam/json/<int:pk>/recreate", views.RapidExamViews.exam_json_recreate, name="exam_json_recreate"),
path("create/", views.RapidCreate.as_view(), name="rapid_create"), path("create/", views.RapidCreate.as_view(), name="rapid_create"),
path("create/defaults", path("create/defaults",
-67
View File
@@ -699,73 +699,6 @@ def mark(request, pk, sk):
) )
def exam_json(request, pk):
exam = get_object_or_404(Exam, pk=pk)
if not exam.active:
raise Http404("No available exam")
exam_json_cache = cache.get("rapids_exam_json_{}".format(pk))
if exam_json_cache is not None and not exam.recreate_json:
exam_json_cache["cached"] = True
return JsonResponse(exam_json_cache)
questions = exam.exam_questions.all()
exam_questions = defaultdict(dict)
exam_order = []
for q in questions:
exam_order.append(q.id)
# Loop through rapidimage associations
images = []
feedback_images = []
for i in q.images.all():
if i.feedback_image == True:
feedback_images.append(image_as_base64(i.image))
# feedback_images.append(i.image.url)
else:
images.append(image_as_base64(i.image))
#images.append(i.image.url)
exam_questions[q.id] = {
"images": images,
#"feedback_image": [],
#"annotations": [str(q.image_annotations)],
"type": "rapid",
}
#if feedback_images:
# exam_questions[q.id]["feedback_image"] = feedback_images
exam_json = {
"eid": "rapid/{}".format(exam.id),
"cached": False,
"exam_type": "rapid",
"exam_name": exam.name,
"exam_mode": True,
"exam_order": exam_order,
"questions": exam_questions,
}
if exam.time_limit:
exam_json["exam_time"] = exam.time_limit
exam.recreate_json = False
exam.save()
cache.set("rapids_exam_json_{}".format(pk), exam_json, 3600)
return JsonResponse(exam_json)
@login_required @login_required
def exam_scores_cid(request, pk): def exam_scores_cid(request, pk):
exam = get_object_or_404(Exam, pk=pk) exam = get_object_or_404(Exam, pk=pk)
+1
View File
@@ -15,3 +15,4 @@ django-sortedm2m
pytesseract pytesseract
opencv-python opencv-python
pydicom pydicom
django-dbbackup