furher shorts improvements
This commit is contained in:
+2
-12
@@ -52,6 +52,7 @@ from generic.models import (
|
||||
Examination,
|
||||
# Condition,
|
||||
ExamBase,
|
||||
FindingBase,
|
||||
Plane,
|
||||
Contrast,
|
||||
QuestionNote,
|
||||
@@ -600,29 +601,18 @@ class SeriesImage(SeriesImageBase):
|
||||
# }}
|
||||
|
||||
|
||||
class SeriesFinding(models.Model):
|
||||
class SeriesFinding(FindingBase):
|
||||
series = models.ForeignKey(
|
||||
"Series", related_name="findings", on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
description = models.TextField(
|
||||
null=True, blank=True, help_text="Findings on the series / stack"
|
||||
)
|
||||
findings = models.ManyToManyField(Finding, blank=True)
|
||||
structures = models.ManyToManyField(Structure, blank=True)
|
||||
conditions = models.ManyToManyField(Condition, blank=True)
|
||||
annotation_json = models.TextField(null=True, blank=True)
|
||||
viewport_json = models.TextField(null=True, blank=True)
|
||||
# This has been migrated to install the image id (not index)
|
||||
current_image_id_index = models.TextField(null=True, blank=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
findings = self.findings.all().values_list("name")
|
||||
return f"SeriesFinding: {self.series.id}/{findings}/{self.description}"
|
||||
|
||||
def annotation_as_string(self) -> str:
|
||||
return json.dumps(self.annotation_json)
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Series(SeriesBase):
|
||||
modality = models.ForeignKey(
|
||||
|
||||
+29
-1
@@ -1963,4 +1963,32 @@ class CimarSeriesThumbnail(models.Model):
|
||||
image = models.ImageField(upload_to="cimar_series_thumbnails")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.series_id}"
|
||||
return f"{self.series_id}"
|
||||
|
||||
|
||||
class FindingBase(models.Model):
|
||||
description = models.TextField(
|
||||
null=True, blank=True, help_text="Findings on the question"
|
||||
)
|
||||
annotation_json = models.TextField(null=True, blank=True)
|
||||
viewport_json = models.TextField(null=True, blank=True)
|
||||
# This has been migrated to install the image id (not index)
|
||||
current_image_id_index = models.TextField(null=True, blank=True)
|
||||
|
||||
# TODO add tags
|
||||
# tags = models.ManyToManyField(Tag, blank=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return self.description
|
||||
|
||||
def annotation_as_string(self) -> str:
|
||||
return json.dumps(self.annotation_json)
|
||||
|
||||
def get_pretty_annotation_json(self):
|
||||
return get_pretty_json(json.loads(self.annotation_json))
|
||||
|
||||
def get_pretty_viewport_json(self):
|
||||
return get_pretty_json(json.loads(self.viewport_json))
|
||||
@@ -6,6 +6,7 @@ from anatomy.api import router as anatomy_router
|
||||
from sbas.api import router as sbas_router
|
||||
from physics.api import router as physics_router
|
||||
from atlas.api import router as atlas_router
|
||||
from shorts.api import router as shorts_router
|
||||
|
||||
api = NinjaAPI(version="1")
|
||||
|
||||
@@ -17,6 +18,7 @@ api.add_router("longs/", longs_router)
|
||||
api.add_router("sbas/", sbas_router)
|
||||
api.add_router("physics/", physics_router)
|
||||
api.add_router("atlas/", atlas_router)
|
||||
api.add_router("shorts/", shorts_router)
|
||||
|
||||
@api.get("/hello")
|
||||
def hello(request):
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
{% endif %}>
|
||||
{{ answer }}
|
||||
</span>
|
||||
<td>
|
||||
<td>{{answer.status}}</td>
|
||||
</td>
|
||||
<td>{{answer.status}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from typing import List
|
||||
from django.shortcuts import get_object_or_404
|
||||
from ninja import ModelSchema, Router
|
||||
from .models import Question, Exam
|
||||
|
||||
from .decorators import user_is_author_or_shorts_checker
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from generic.decorators import check_user_in_group
|
||||
from generic.constants import Group
|
||||
|
||||
from ninja.security import django_auth
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class QuestionSchema(ModelSchema):
|
||||
class Config:
|
||||
model = Question
|
||||
|
||||
#model_fields = ["question", "history", "feedback", "normal", "laterality"]
|
||||
model_fields = "__all__"
|
||||
|
||||
#model_exclude = ["answers"]
|
||||
|
||||
class ExamSchema(ModelSchema):
|
||||
class Config:
|
||||
model = Exam
|
||||
|
||||
model_fields = ["id", "name", "active", "publish_results"]
|
||||
|
||||
@router.get('/')
|
||||
def list_questions(request):
|
||||
return [
|
||||
{"id": e.id, "normal": e.normal}
|
||||
for e in Question.objects.all()
|
||||
]
|
||||
|
||||
@router.get('/question/{question_id}', response=QuestionSchema)
|
||||
@check_user_in_group(Group.cid_user_manager)
|
||||
def get_rapid_details(request, question_id: int):
|
||||
|
||||
rapid = get_object_or_404(Question, id=question_id)
|
||||
return rapid
|
||||
|
||||
|
||||
@router.get('/user_exams', response=List[ExamSchema], url_name="shorts_user_exams")
|
||||
def user_exams(request):
|
||||
"""Returns a list of exams that the user has access to"""
|
||||
user = request.user
|
||||
if user.groups.filter(name="shorts_checker").exists():
|
||||
return Exam.objects.filter(archive=False).order_by('name')
|
||||
|
||||
return Exam.objects.filter(author__id=user.id, archive=False).order_by('name')
|
||||
+57
-22
@@ -13,6 +13,7 @@ from shorts.models import (
|
||||
Abnormality,
|
||||
Examination,
|
||||
Question,
|
||||
QuestionFinding,
|
||||
QuestionImage,
|
||||
Region,
|
||||
SampleAnswer,
|
||||
@@ -29,10 +30,15 @@ from crispy_forms.layout import Submit, Layout, Div, Field, HTML
|
||||
from crispy_forms.bootstrap import InlineRadios
|
||||
|
||||
|
||||
class QuestionAnswerForm(ModelForm):
|
||||
from dal import autocomplete
|
||||
|
||||
from autocomplete import widgets as htmx_widgets, Autocomplete, AutocompleteWidget, ModelAutocomplete, register as autocomplete_register
|
||||
|
||||
|
||||
class QuestionSampleAnswerForm(ModelForm):
|
||||
class Meta:
|
||||
model = UserAnswer
|
||||
fields = ("answer",)
|
||||
model = Question
|
||||
fields = []
|
||||
|
||||
|
||||
class MarkQuestionForm(Form):
|
||||
@@ -42,10 +48,10 @@ class MarkQuestionForm(Form):
|
||||
marked_answers = CharField(required=False)
|
||||
|
||||
|
||||
class QuestionAnswerForm(ModelForm):
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = []
|
||||
#class QuestionAnswerForm(ModelForm):
|
||||
# class Meta:
|
||||
# model = Question
|
||||
# fields = []
|
||||
|
||||
class QuestionForm(ModelForm):
|
||||
|
||||
@@ -93,6 +99,7 @@ class QuestionForm(ModelForm):
|
||||
Field('examination', css_class='form-control'),
|
||||
Div(InlineRadios('laterality', css_class='form-control'), css_class='clearfix clear-both'),
|
||||
Field('history', css_class='form-control'),
|
||||
Field('marking_guidance', css_class='form-control'),
|
||||
Field('feedback', css_class='form-control'),
|
||||
Field('open_access', css_class='form-control'),
|
||||
HTML("<h4 class='clear-both' id='exams-label'>Exams</h4>"),
|
||||
@@ -188,6 +195,7 @@ class QuestionForm(ModelForm):
|
||||
# "site",
|
||||
"feedback",
|
||||
"history",
|
||||
"marking_guidance",
|
||||
"open_access",
|
||||
]
|
||||
# fields = ['question', 'feedback', 'subspecialty', 'references']
|
||||
@@ -217,20 +225,20 @@ ImageFormSet = inlineformset_factory(
|
||||
# max_num=10,
|
||||
#)
|
||||
#
|
||||
#AnswerUpdateFormSet = inlineformset_factory(
|
||||
# Question,
|
||||
# SampleAnswer,
|
||||
# fields=["answer", "status"],
|
||||
# widgets={
|
||||
# "answer": Textarea(
|
||||
# attrs={"required": "false", "minlength": 2, "maxlength": 500, "rows": 3}
|
||||
# )
|
||||
# },
|
||||
# exclude=[],
|
||||
# can_delete=True,
|
||||
# extra=0,
|
||||
# max_num=10,
|
||||
#)
|
||||
SampleAnswerUpdateFormSet = inlineformset_factory(
|
||||
Question,
|
||||
SampleAnswer,
|
||||
fields=["answer", "score", "proposed"],
|
||||
widgets={
|
||||
"answer": Textarea(
|
||||
attrs={"required": "false", "minlength": 2, "maxlength": 500, "rows": 3}
|
||||
)
|
||||
},
|
||||
exclude=[],
|
||||
can_delete=True,
|
||||
extra=0,
|
||||
max_num=10,
|
||||
)
|
||||
|
||||
|
||||
# This should be made generic?
|
||||
@@ -248,4 +256,31 @@ class ExamMarkerForm(ExamMarkerFormMixin):
|
||||
|
||||
class ExamGroupsForm(ExamGroupsFormMixin):
|
||||
class Meta(ExamGroupsFormMixin.Meta):
|
||||
model = Exam
|
||||
model = Exam
|
||||
|
||||
class QuestionFindingForm(ModelForm):
|
||||
class Meta:
|
||||
model = QuestionFinding
|
||||
exclude = ["question", "annotation_json", "viewport_json", "current_image_id_index"]
|
||||
|
||||
widgets = {
|
||||
"findings": autocomplete.ModelSelect2Multiple(
|
||||
url="atlas:finding-autocomplete"
|
||||
),
|
||||
"structures": autocomplete.ModelSelect2Multiple(
|
||||
url="atlas:structure-autocomplete"
|
||||
),
|
||||
"conditions": autocomplete.ModelSelect2Multiple(
|
||||
url="atlas:condition-autocomplete"
|
||||
),
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs.get("question_id"):
|
||||
initial = kwargs.setdefault("initial", {})
|
||||
initial["question"] = kwargs.pop("question_id")
|
||||
|
||||
super(QuestionFindingForm, self).__init__(*args, **kwargs)
|
||||
|
||||
ModelForm.__init__(self, *args, **kwargs)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-07 08:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shorts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='marking_guidance',
|
||||
field=models.TextField(blank=True, help_text='Marking guidance for the question. This is not shown to the candidate but is used by the marker to help them mark the question.', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='history',
|
||||
field=models.TextField(blank=True, help_text='Single line history for the question, e.g. Age 14, male. Referral from ED. History: Painful wrist after falling off skateboard.', null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-07 09:50
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('atlas', '0067_case_cimar_uuid'),
|
||||
('shorts', '0002_question_marking_guidance_alter_question_history'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QuestionFinding',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.TextField(blank=True, help_text='Findings on the question', null=True)),
|
||||
('annotation_json', models.TextField(blank=True, null=True)),
|
||||
('viewport_json', models.TextField(blank=True, null=True)),
|
||||
('current_image_id_index', models.TextField(blank=True, null=True)),
|
||||
('conditions', models.ManyToManyField(blank=True, to='atlas.condition')),
|
||||
('findings', models.ManyToManyField(blank=True, to='atlas.finding')),
|
||||
('question', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='findings', to='shorts.question')),
|
||||
('structures', models.ManyToManyField(blank=True, to='atlas.structure')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.4 on 2025-04-07 10:10
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shorts', '0003_questionfinding'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='questionimage',
|
||||
name='image_annotations',
|
||||
),
|
||||
]
|
||||
+109
-25
@@ -1,14 +1,28 @@
|
||||
from collections import defaultdict
|
||||
import os
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
import pydicom
|
||||
from atlas.models import Finding, Structure, Condition
|
||||
from rad.settings import REMOTE_URL
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import dicognito
|
||||
import json
|
||||
|
||||
# Create your models here.
|
||||
from generic.models import CidUser, CidUserGroup, ExamBase, ExamCollection, ExamUserStatus, QuestionBase, UserAnswerBase, UserUserGroup
|
||||
from generic.models import (
|
||||
CidUser,
|
||||
CidUserGroup,
|
||||
ExamBase,
|
||||
ExamCollection,
|
||||
ExamUserStatus,
|
||||
FindingBase,
|
||||
QuestionBase,
|
||||
UserAnswerBase,
|
||||
UserUserGroup,
|
||||
get_pretty_json,
|
||||
)
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.utils.html import mark_safe
|
||||
@@ -17,16 +31,20 @@ from rapids.models import Abnormality, Examination, Region
|
||||
|
||||
from helpers.images import get_image_hash, image_as_base64
|
||||
|
||||
|
||||
def image_directory_path(instance, filename):
|
||||
# return u"{0}".format(filename)
|
||||
return "shorts/picture/{0}".format(filename)
|
||||
|
||||
class Question(QuestionBase):
|
||||
"""
|
||||
"""
|
||||
|
||||
history = models.TextField(null=True, blank=True,
|
||||
help_text="Single line history for the question, e.g. Age 14, male. Referral from ED. History: Painful wrist after falling off skateboard.")
|
||||
class Question(QuestionBase):
|
||||
""" """
|
||||
|
||||
history = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Single line history for the question, e.g. Age 14, male. Referral from ED. History: Painful wrist after falling off skateboard.",
|
||||
)
|
||||
|
||||
NONE = "NONE"
|
||||
LEFT = "LEFT"
|
||||
@@ -60,6 +78,12 @@ class Question(QuestionBase):
|
||||
help_text="Applies to the answer, not the examination",
|
||||
)
|
||||
|
||||
marking_guidance = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Marking guidance for the question. This is not shown to the candidate but is used by the marker to help them mark the question.",
|
||||
)
|
||||
|
||||
author = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank=True,
|
||||
@@ -70,6 +94,9 @@ class Question(QuestionBase):
|
||||
def get_app_name(self):
|
||||
return "shorts"
|
||||
|
||||
def get_findings_url(self):
|
||||
return reverse("shorts:question_findings", kwargs={"question_id": self.pk})
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("shorts:question_detail", kwargs={"pk": self.pk})
|
||||
|
||||
@@ -92,10 +119,10 @@ class Question(QuestionBase):
|
||||
# def GetNonFeedbackQuestionImages(self):
|
||||
# return self.get_images()
|
||||
|
||||
def get_image_annotations(self):
|
||||
return json.dumps(
|
||||
[i.image_annotations for i in self.images.all() if not i.feedback_image]
|
||||
)
|
||||
#def get_image_annotations(self):
|
||||
# return json.dumps(
|
||||
# [i.image_annotations for i in self.images.all() if not i.feedback_image]
|
||||
# )
|
||||
|
||||
def get_laterality_string(self):
|
||||
if self.laterality == self.NONE:
|
||||
@@ -122,15 +149,31 @@ class Question(QuestionBase):
|
||||
except pydicom.errors.InvalidDicomError:
|
||||
pass
|
||||
|
||||
def check_user_can_edit(self, user):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
if self.author.filter(id=user.id).exists():
|
||||
return True
|
||||
|
||||
if user.groups.filter(name="shorts_checker").exists():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class QuestionImage(models.Model):
|
||||
question = models.ForeignKey(Question, related_name="images", on_delete=models.CASCADE)
|
||||
question = models.ForeignKey(
|
||||
Question, related_name="images", on_delete=models.CASCADE
|
||||
)
|
||||
image = models.FileField(upload_to=image_directory_path)
|
||||
|
||||
image_annotations = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="Stores a JSON representation of annotations to be applied by cornerstonetools",
|
||||
)
|
||||
# Image annotations are stored as findings in shorts cases
|
||||
#image_annotations = models.TextField(
|
||||
# blank=True,
|
||||
# null=True,
|
||||
# help_text="Stores a JSON representation of annotations to be applied by cornerstonetools",
|
||||
#)
|
||||
|
||||
feedback_image = models.BooleanField(default=False)
|
||||
|
||||
@@ -162,7 +205,9 @@ class QuestionImage(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
"""Override save method to add image hash"""
|
||||
if self.image:
|
||||
image_hash, is_dicom = get_image_hash(self.image, hash_type="md5", direct_pixel_data=False)
|
||||
image_hash, is_dicom = get_image_hash(
|
||||
self.image, hash_type="md5", direct_pixel_data=False
|
||||
)
|
||||
self.is_dicom = is_dicom
|
||||
self.image_md5_hash = image_hash
|
||||
|
||||
@@ -170,19 +215,29 @@ class QuestionImage(models.Model):
|
||||
if image_hash != "12345ABCD":
|
||||
super().save(*args, **kwargs) # Call the "real" save() method.
|
||||
|
||||
|
||||
class SampleAnswer(models.Model):
|
||||
"""Model that defines sample answers for the question.
|
||||
|
||||
These are used to aid marking and for feedback purposes
|
||||
|
||||
"""
|
||||
|
||||
question = models.ForeignKey(
|
||||
Question, related_name="sample_answers", on_delete=models.CASCADE
|
||||
)
|
||||
answer = models.TextField()
|
||||
|
||||
score = models.IntegerField(default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)])
|
||||
score = models.IntegerField(
|
||||
default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)]
|
||||
)
|
||||
|
||||
proposed = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.answer
|
||||
|
||||
|
||||
class ExamQuestionDetail(models.Model):
|
||||
exam = models.ForeignKey("Exam", on_delete=models.CASCADE)
|
||||
question = models.ForeignKey(Question, on_delete=models.CASCADE)
|
||||
@@ -192,10 +247,13 @@ class ExamQuestionDetail(models.Model):
|
||||
class Meta:
|
||||
ordering = ("sort_order",)
|
||||
|
||||
|
||||
class Exam(ExamBase):
|
||||
app_name = "shorts"
|
||||
|
||||
exam_questions = models.ManyToManyField(Question, through=ExamQuestionDetail, related_name="exams")
|
||||
exam_questions = models.ManyToManyField(
|
||||
Question, through=ExamQuestionDetail, related_name="exams"
|
||||
)
|
||||
|
||||
time_limit = models.IntegerField(
|
||||
help_text="Exam time limit (in seconds). Default is 2 hours",
|
||||
@@ -238,11 +296,20 @@ class Exam(ExamBase):
|
||||
related_name="shorts_user_user_groups",
|
||||
)
|
||||
|
||||
exam_user_status = GenericRelation(ExamUserStatus, related_query_name="shorts_exams")
|
||||
cid_user_exam = GenericRelation("generic.CidUserExam", related_query_name="shorts_exams")
|
||||
|
||||
examcollection = models.ForeignKey(ExamCollection, blank=True, null=True, on_delete=models.SET_NULL, related_name="shorts_exams")
|
||||
exam_user_status = GenericRelation(
|
||||
ExamUserStatus, related_query_name="shorts_exams"
|
||||
)
|
||||
cid_user_exam = GenericRelation(
|
||||
"generic.CidUserExam", related_query_name="shorts_exams"
|
||||
)
|
||||
|
||||
examcollection = models.ForeignKey(
|
||||
ExamCollection,
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="shorts_exams",
|
||||
)
|
||||
|
||||
def get_app_name(self):
|
||||
return "shorts"
|
||||
@@ -325,6 +392,7 @@ class Exam(ExamBase):
|
||||
def get_absolute_url(self):
|
||||
return reverse("shorts:exam_overview", kwargs={"pk": self.pk})
|
||||
|
||||
|
||||
class UserAnswer(UserAnswerBase):
|
||||
"""User answers by candidate"""
|
||||
|
||||
@@ -349,7 +417,9 @@ class UserAnswer(UserAnswerBase):
|
||||
Exam, related_name="cid_user_answers", on_delete=models.CASCADE, null=True
|
||||
)
|
||||
|
||||
score = models.IntegerField(default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)])
|
||||
score = models.IntegerField(
|
||||
default=0, blank=True, validators=[MinValueValidator(0), MaxValueValidator(5)]
|
||||
)
|
||||
|
||||
class CallStateOptions(models.TextChoices):
|
||||
CORRECT = "C", _("Correct Call")
|
||||
@@ -398,4 +468,18 @@ class UserAnswer(UserAnswerBase):
|
||||
return self.callstate.label
|
||||
|
||||
def get_answer_score(self, cached=True):
|
||||
return self.score
|
||||
return self.score
|
||||
|
||||
|
||||
class QuestionFinding(FindingBase):
|
||||
question = models.ForeignKey(
|
||||
Question, related_name="findings", on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
findings = models.ManyToManyField(Finding, blank=True)
|
||||
structures = models.ManyToManyField(Structure, blank=True)
|
||||
conditions = models.ManyToManyField(Condition, blank=True)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
findings = self.findings.all().values_list("name")
|
||||
return f"Findings: {self.question.id}/{findings}/{self.description}"
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a href='{% url "shorts:question_findings" question_id=question.pk %}'>Click here to view/add findings</a>
|
||||
<div>
|
||||
Exam(s): {% for exam in question.exams.all %}
|
||||
<a href="{% url 'shorts:exam_overview' pk=exam.pk %}">{{ exam }}</a>,
|
||||
@@ -40,9 +41,28 @@
|
||||
<p class="pre-whitespace"><b>Feedback:</b> {{ question.feedback }}</p>
|
||||
<p><b>Author(s):</b> {% for author in question.author.all %} <a
|
||||
href="{% url 'shorts:author_detail' pk=author.pk %}">{{author}}</a>, {% endfor %}</p>
|
||||
<p><b>Checked by:</b> {% for verified in question.verified.all %} <a
|
||||
href="{% url 'shorts:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
||||
<p class="pre-whitespace">
|
||||
<details>
|
||||
<summary>
|
||||
Sample Answers:
|
||||
</summary>
|
||||
<table>
|
||||
<tr><th>Answer</th><th>Score</th></tr>
|
||||
{% for answer in question.sample_answers.all|dictsortreversed:"score" %}
|
||||
<tr>
|
||||
<td>
|
||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="shorts"
|
||||
{% endif %}>
|
||||
{{ answer }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{answer.score}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</details>
|
||||
</p>
|
||||
{% comment %} <p class="pre-whitespace">
|
||||
<details>
|
||||
<summary>
|
||||
Answers:
|
||||
@@ -62,49 +82,15 @@
|
||||
{% endfor %}
|
||||
</table>
|
||||
</details>
|
||||
</p>
|
||||
</p> {% endcomment %}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
{% if view_feedback %}
|
||||
{% include 'question_notes.html' %}
|
||||
{% if not question.normal %}
|
||||
<details>
|
||||
<summary>
|
||||
Suggested answers
|
||||
</summary>
|
||||
<ul class="suggested_answers">
|
||||
{% for ans in question.get_suggested_answers %}
|
||||
<li data-string="{{ans}}">{{ans}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<details open>
|
||||
<summary>
|
||||
Image viewer
|
||||
</summary>
|
||||
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ question.get_image_url_array }}"
|
||||
data-annotations='{{question.get_image_annotations}}'>
|
||||
</div>
|
||||
<button id="save-annotations" class="save-shorts-annotations">Save Annotations</button>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Image annotations:
|
||||
</summary>
|
||||
{% for image in question.images.all %}
|
||||
<span class="image-block">
|
||||
Image {{ forloop.counter }}: {{image.image_annotations}}<br />
|
||||
</span>
|
||||
{% endfor %}
|
||||
</details>
|
||||
<a href="{% url 'shorts:question_anonymise_dicom' pk=question.pk %}"
|
||||
title="Anonymise dicom images">Anonymise dicoms</a><br />
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#edit-button").click(() => {
|
||||
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
{% extends 'shorts/base.html' %}
|
||||
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block content %}
|
||||
{% include "shorts/question_link_header.html" %}
|
||||
<p>The viewer shown below can be used to annotate the images. The viewport can be zoomed and paned to highlight the abnormality which can then be indicated with arrows or bounding boxes.</p>
|
||||
|
||||
<div id="single-dicom-viewer" class="dicom-viewer" data-images="{{ question.get_image_url_array }}">
|
||||
</div>
|
||||
|
||||
{% if can_edit %}
|
||||
{% if editing_finding < 1 %}
|
||||
<button id="add-finding-button">Add finding</button>
|
||||
{% endif %}
|
||||
<button id="reset-viewport-button">Reset viewport</button>
|
||||
<div id="finding-form">
|
||||
<div class="hide" id="hidden-form">
|
||||
<form method="post" id="question_finding_form">
|
||||
{% csrf_token %}
|
||||
{{question_finding_form|crispy}}
|
||||
<input type="submit" value="Submit">
|
||||
<button id="cancel-add-finding-button">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<button id="reset-viewport-button">Reset viewport</button>
|
||||
{% endif %}
|
||||
|
||||
<details open>
|
||||
<summary>Findings</summary>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% for finding in question.findings.all %}
|
||||
<div class="finding-box">
|
||||
<button class="view-finding-button" data-annotationjson={{finding.annotation_json}}
|
||||
data-viewportjson={{finding.viewport_json}} data-findingid={{finding.id}} data-currentimageid={{finding.current_image_id_index}}>Click to view</button>
|
||||
<span class="view-finding-details">
|
||||
Finding(s): {% for f in finding.findings.all %}{{f.get_link}}{% endfor %}<br />
|
||||
Structure(s): {% for s in finding.structures.all %}{{s.get_link}}{% endfor %}<br />
|
||||
Condition(s): {% for s in finding.conditions.all %}{{s.get_link}}{% endfor %}<br />
|
||||
Description: {{finding.description}}<br />
|
||||
|
||||
{% if request.user.is_superuser %}
|
||||
<span _='on click toggle .hidden on next .extra-details'>+</span>
|
||||
<div class="hidden extra-details">
|
||||
<h4>Annotation JSON</h4>
|
||||
<pre>{{finding.get_pretty_annotation_json}}</pre>
|
||||
<h4>Viewport JSON</h4>
|
||||
<pre>{{finding.get_pretty_viewport_json}}</pre>
|
||||
<h4>Image ID</h4>
|
||||
<pre>{{finding.current_image_id_index}}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if can_edit %}
|
||||
<a href="{% url 'shorts:question_edit_finding' question_id=question.pk finding_pk=finding.pk %}" class="edit-finding-link">Edit</a>
|
||||
<a href="{% url 'shorts:delete_finding' pk=finding.pk %}" class="delete-finding-link">Delete</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block js %}
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#add-finding-button").click(() => {
|
||||
console.log("add finding button clicked")
|
||||
$("#hidden-form").show()
|
||||
dicom_element = $(".cornerstone-element").get(0);
|
||||
//cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
||||
//cornerstone.reset(dicom_element);
|
||||
////cornerstone.resize(dicom_element, true);
|
||||
$("#add-finding-button").hide()
|
||||
//$("#finding-form").empty().append(
|
||||
// $("#hidden-form form").clone()
|
||||
//);
|
||||
|
||||
});
|
||||
$("#cancel-add-finding-button").click((e) => {
|
||||
$("#hidden-form").hide()
|
||||
$("#add-finding-button").show()
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
|
||||
$("#reset-viewport-button").click(() => {
|
||||
dicom_element = $(".cornerstone-element").get(0);
|
||||
//annotationjson = JSON.parse(e.currentTarget.dataset.annotationjson);
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
||||
cornerstone.reset(dicom_element);
|
||||
cornerstone.resize(dicom_element, true);
|
||||
|
||||
});
|
||||
|
||||
$(".view-finding-button").each((n, el) => {
|
||||
$(el).click((e) => {
|
||||
dicom_element = $(".cornerstone-element").get(0);
|
||||
annotationjson = JSON.parse(e.currentTarget.dataset.annotationjson);
|
||||
viewport = JSON.parse(e.currentTarget.dataset.viewportjson);
|
||||
current_image_id_index = e.currentTarget.dataset.currentimageid;
|
||||
console.log(e.currentTarget, current_image_id_index)
|
||||
loadAnnotationAndViewportOnElement(annotationjson, viewport, dicom_element, current_image_id_index);
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
{% if editing_finding > 0 %}
|
||||
$("#hidden-form").show()
|
||||
setTimeout(function() {
|
||||
$(".view-finding-button[data-findingid={{editing_finding}}]").trigger("click");
|
||||
}, 1000)
|
||||
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
function loadAnnotationAndViewportOnElement(annotation, viewport, dicom_element, current_image_id_index) {
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.clear(dicom_element);
|
||||
cornerstone.getEnabledElement(dicom_element).viewport = viewport
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
|
||||
annotation);
|
||||
console.log("annotation", annotation)
|
||||
console.log("viewport", viewport)
|
||||
|
||||
//cornerstone.getEnabledElement(dicom_element).toolStateManager.restoreToolState(annotationjson)
|
||||
cornerstone.resize(dicom_element);
|
||||
console.log("current_image_id_index", current_image_id_index)
|
||||
//current_image_id_index = parseInt(current_image_id_index);
|
||||
|
||||
console.log(current_image_id_index, Number.isInteger(current_image_id_index))
|
||||
if (Number.isInteger(current_image_id_index)) {
|
||||
console.log("loading stack index")
|
||||
dicomViewer.loadStackIndex(current_image_id_index, dicom_element);
|
||||
} else {
|
||||
try {
|
||||
console.log("current_image_id_index", current_image_id_index)
|
||||
dicomViewer.loadImageById(current_image_id_index, dicom_element);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
console.log("loading image by id failed")
|
||||
console.log("loading next annotation image")
|
||||
dicomViewer.getNextAnnotationImage(dicom_element);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$(document).on('submit', '#question_finding_form', function (e) {
|
||||
dicom_element = $(".cornerstone-element").get(0);
|
||||
c = cornerstone.getEnabledElement(dicom_element);
|
||||
e.preventDefault();
|
||||
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '{% url "shorts:add_finding" %}',
|
||||
data: {
|
||||
question_finding_id: {{ editing_finding }},
|
||||
description: $('#id_description').val(),
|
||||
question: {{ question.pk }},
|
||||
//annotation_json: JSON.stringify(c.toolStateManager.saveToolState()),
|
||||
// This is the stack number
|
||||
//current_image_id_index: c.toolStateManager.toolState.stack.data[0].currentImageIdIndex,
|
||||
// but we want to use the current image id (so if the stack is reordered it still attaches to the correct image)
|
||||
current_image_id_index: c.toolStateManager.toolState.stack.data[0].imageIds[c.toolStateManager.toolState.stack.data[0].currentImageIdIndex],
|
||||
annotation_json: JSON.stringify(cornerstoneTools.globalImageIdSpecificToolStateManager
|
||||
.saveToolState()),
|
||||
viewport_json: JSON.stringify(c.viewport),
|
||||
findings: JSON.stringify($('#finding-form select[name="findings"]').find(":selected")
|
||||
.map((i, el) => {
|
||||
return $(el).val()
|
||||
}).toArray()),
|
||||
structures: JSON.stringify($('#finding-form select[name="structures"]').find(
|
||||
":selected")
|
||||
.map((i, el) => {
|
||||
return $(el).val()
|
||||
}).toArray()),
|
||||
conditions: JSON.stringify($('#finding-form select[name="conditions"]').find(
|
||||
":selected")
|
||||
.map((i, el) => {
|
||||
return $(el).val()
|
||||
}).toArray()),
|
||||
csrfmiddlewaretoken: $('input[name=csrfmiddlewaretoken]').val(),
|
||||
action: 'post'
|
||||
},
|
||||
success: function (json) {
|
||||
$("#finding-form").empty();
|
||||
toastr.info('Finding added.');
|
||||
location.href='{{question.get_findings_url}}';
|
||||
},
|
||||
error: function (xhr, errmsg, err) {
|
||||
console.log(xhr.status + ": " + xhr
|
||||
.responseText); // provide a bit more info about the error to the console
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock js %}
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
<div class="floating-header">
|
||||
|
||||
<a href="{% url 'shorts:question_detail' pk=question.pk %}" title="View the Question">View</a>
|
||||
<a href="{% url 'shorts:question_update' pk=question.pk %}" title="Edit the Question">Edit</a>
|
||||
<a href="{% url 'shorts:question_clone' pk=question.pk %}" title="Clone the Question (duplicate everything but the images)">Clone</a>
|
||||
<a href="{% url 'shorts:question_delete' pk=question.pk %}" title="Delete the Question">Delete</a>
|
||||
<a href="{% url 'shorts:question_answer_update' pk=question.pk %}" title="Update the question answers">Edit Answers</a>
|
||||
<a href="{% url 'shorts:question_sample_answers' pk=question.pk %}" title="Update the question sample answers">Edit Sample Answers</a>
|
||||
<a href="{% url 'shorts:question_findings' question_id=question.pk %}" title="Editing the question findings">Edit Findings</a>
|
||||
<a href="#" onclick="return window.create_popup_window('{% url 'feedback_create' question_type='rapid' pk=question.pk %}')"> Add Note</a>
|
||||
{% if request.user.is_superuser %}
|
||||
<a href="{% url 'admin:shorts_question_change' question.id %}" title="Edit the Question using the admin interface">Admin Edit</a>
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
{% extends "shorts/base.html" %}
|
||||
{% load static from static %}
|
||||
|
||||
{% block js %}
|
||||
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
||||
|
||||
<script type="text/javascript">
|
||||
let csrf_token = "{{csrf_token}}";
|
||||
|
||||
function add_answers_input_form() {
|
||||
var form_idx = $('#id_sample_answers-TOTAL_FORMS').val();
|
||||
$('#answer_form_set').append($('#answer_empty_form').html().replace(/__prefix__/g, form_idx));
|
||||
$(`#id_sample_answers-${form_idx}-status`).val(2);
|
||||
$('#id_sample_answers-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
$('#add_more_answers').click(() => {
|
||||
add_answers_input_form()
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{{ form.media }}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% if object %}
|
||||
{% include "shorts/question_link_header.html" %}
|
||||
{% endif %}
|
||||
|
||||
<h2>Update Sample Answers</h2>
|
||||
<p>This page allows you to update sample answers for the question.</p>
|
||||
|
||||
<p>Sample answers are used to aid marking of a question and providing feedback to candidates.</p>
|
||||
|
||||
<p>Ideally there should be a selection of answers that cover the available scores</p>
|
||||
|
||||
|
||||
<form action="" method="post" enctype="multipart/form-data" id="shorts-form">
|
||||
{% csrf_token %}
|
||||
<h3>Answers:</h3>
|
||||
<div id="answer_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="button" value="Add More Answers" id="add_more_answers">
|
||||
<p>
|
||||
<input type="submit" class="submit-button btn btn-primary" value="Submit" name="submit">
|
||||
</p>
|
||||
</form>
|
||||
<div id="answer_empty_form" style="display:none">
|
||||
<ul class='no_error answer-formset'>
|
||||
{{ answer_formset.empty_form.as_ul }}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
+29
-3
@@ -82,10 +82,15 @@ urlpatterns = [
|
||||
name="question_update",
|
||||
),
|
||||
path(
|
||||
"question/<int:pk>/update_answers",
|
||||
views.QuestionAnswerUpdate.as_view(),
|
||||
name="question_answer_update",
|
||||
"question/<int:pk>/sample_answers",
|
||||
views.QuestionSampleAnswerUpdate.as_view(),
|
||||
name="question_sample_answers",
|
||||
),
|
||||
#path(
|
||||
# "question/<int:pk>/update_answers",
|
||||
# views.QuestionAnswerUpdate.as_view(),
|
||||
# name="question_answer_update",
|
||||
#),
|
||||
path(
|
||||
"user_answers/",
|
||||
views.UserAnswerTableView.as_view(),
|
||||
@@ -99,6 +104,27 @@ urlpatterns = [
|
||||
views.UserAnswerDelete.as_view(),
|
||||
name="user_answer_delete",
|
||||
),
|
||||
path(
|
||||
"question/<int:question_id>/findings",
|
||||
views.question_findings,
|
||||
name="question_findings",
|
||||
),
|
||||
|
||||
path(
|
||||
"question/add_finding",
|
||||
views.create_findings,
|
||||
name="add_finding",
|
||||
),
|
||||
path(
|
||||
"question/delete_finding/<int:pk>",
|
||||
views.FindingDelete.as_view(),
|
||||
name="delete_finding",
|
||||
),
|
||||
path(
|
||||
"question/<int:question_id>/<int:finding_pk>",
|
||||
views.question_findings,
|
||||
name="question_edit_finding",
|
||||
),
|
||||
#path(
|
||||
# "answer-autocomplete",
|
||||
# views.AnswerAutocomplete.as_view(
|
||||
|
||||
+128
-7
@@ -37,11 +37,13 @@ from generic.views import (
|
||||
UpdateQuestionMixin,
|
||||
)
|
||||
from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
|
||||
from atlas.models import Finding, Structure, Condition
|
||||
from shorts.decorators import user_is_author_or_shorts_checker
|
||||
|
||||
from .models import (
|
||||
Exam,
|
||||
Question,
|
||||
QuestionFinding,
|
||||
QuestionImage,
|
||||
UserAnswer,
|
||||
SampleAnswer,
|
||||
@@ -53,11 +55,13 @@ from .forms import (
|
||||
ExamAuthorForm,
|
||||
ExamGroupsForm,
|
||||
ExamMarkerForm,
|
||||
QuestionFindingForm,
|
||||
QuestionForm,
|
||||
QuestionAnswerForm,
|
||||
QuestionSampleAnswerForm,
|
||||
MarkQuestionForm,
|
||||
ExamForm,
|
||||
ImageFormSet,
|
||||
SampleAnswerUpdateFormSet,
|
||||
|
||||
)
|
||||
|
||||
@@ -224,18 +228,41 @@ class QuestionCreate(QuestionCreateBase):
|
||||
# return super().form_valid(form)
|
||||
|
||||
|
||||
class QuestionAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
||||
class QuestionSampleAnswerUpdate(AuthorOrCheckerRequiredMixin, UpdateView):
|
||||
model = Question
|
||||
form_class = QuestionAnswerForm
|
||||
template_name = "shorts/shortsquestionanswer_form.html"
|
||||
form_class = QuestionSampleAnswerForm
|
||||
template_name = "shorts/sample_answer_form.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(QuestionAnswerUpdate, self).get_context_data(**kwargs)
|
||||
context = super(QuestionSampleAnswerUpdate, self).get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
context["answer_formset"] = SampleAnswerUpdateFormSet(
|
||||
self.request.POST, self.request.FILES, instance=self.object
|
||||
)
|
||||
context["answer_formset"].full_clean()
|
||||
else:
|
||||
context["answer_formset"] = SampleAnswerUpdateFormSet(instance=self.object)
|
||||
context["question"] = context["object"]
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
return super().form_valid(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)
|
||||
|
||||
|
||||
class QuestionUpdate(
|
||||
@@ -728,4 +755,98 @@ def question_add_exam(request, question_id: int):
|
||||
return HttpResponse(mark_safe(html))
|
||||
|
||||
|
||||
raise PermissionDenied()
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
# At the moment only the series owner / author can delete findings but anyone can create
|
||||
# TODO: duplicated from atlas "create_series_findings"
|
||||
def create_findings(request):
|
||||
# posts = Post.objects.all()
|
||||
response_data = {}
|
||||
|
||||
if request.POST.get("action") == "post":
|
||||
question_finding_id = int(request.POST.get("question_finding_id"))
|
||||
question_id = request.POST.get("question")
|
||||
findings_ids = json.loads(request.POST.get("findings"))
|
||||
structure_ids = json.loads(request.POST.get("structures"))
|
||||
condition_ids = json.loads(request.POST.get("conditions"))
|
||||
description = request.POST.get("description")
|
||||
annotation_json = request.POST.get("annotation_json")
|
||||
viewport_json = request.POST.get("viewport_json")
|
||||
current_image_id_index = request.POST.get("current_image_id_index")
|
||||
|
||||
question = Question.objects.get(pk=question_id)
|
||||
findings = Finding.objects.filter(pk__in=findings_ids)
|
||||
structures = Structure.objects.filter(pk__in=structure_ids)
|
||||
conditions = Condition.objects.filter(pk__in=condition_ids)
|
||||
|
||||
if question_finding_id > 0:
|
||||
sf = get_object_or_404(QuestionFinding, pk=question_finding_id)
|
||||
sf.question = question
|
||||
sf.description = description
|
||||
sf.annotation_json = annotation_json
|
||||
sf.viewport_json = viewport_json
|
||||
else:
|
||||
sf = QuestionFinding.objects.create(
|
||||
question=question,
|
||||
description=description,
|
||||
annotation_json=annotation_json,
|
||||
viewport_json=viewport_json,
|
||||
)
|
||||
|
||||
sf.findings.set(findings)
|
||||
sf.structures.set(structures)
|
||||
sf.conditions.set(conditions)
|
||||
sf.current_image_id_index = current_image_id_index
|
||||
sf.save()
|
||||
return JsonResponse({"success": True})
|
||||
|
||||
return JsonResponse({"success": False})
|
||||
return render(request, "create_post.html", {"posts": posts})
|
||||
|
||||
# TODO fix permissions
|
||||
class FindingDelete(RevisionMixin, DeleteView): # PermissionRequiredMixin,
|
||||
#permission_required = "atlas.delete_seriesfinding"
|
||||
model = QuestionFinding
|
||||
template_name = "confirm_delete.html"
|
||||
# success_url = reverse_lazy("atlas:case_view")
|
||||
|
||||
def get_object(self):
|
||||
question_finding = super().get_object()
|
||||
question = question_finding.question
|
||||
if question.check_user_can_edit(self.request.user):
|
||||
return question_finding
|
||||
else:
|
||||
raise PermissionDenied
|
||||
|
||||
def get_success_url(self):
|
||||
pk = self.kwargs["pk"]
|
||||
question_id = get_object_or_404(QuestionFinding, pk=pk).question.id
|
||||
return reverse("shorts:question_detail", kwargs={"pk": question_id})
|
||||
|
||||
def question_findings(request, question_id, finding_pk=None):
|
||||
question = get_object_or_404(Question, pk=question_id)
|
||||
|
||||
#can_edit = series.check_user_can_edit(request.user)
|
||||
can_edit = True
|
||||
|
||||
editing_finding = -1
|
||||
if finding_pk is not None:
|
||||
finding = get_object_or_404(QuestionFinding, pk=finding_pk)
|
||||
question_finding_form = QuestionFindingForm(None, instance=finding)
|
||||
editing_finding = finding.pk
|
||||
else:
|
||||
question_finding_form = QuestionFindingForm(question_id=question.id)
|
||||
|
||||
# logging.debug(atlas.subspecialty.first().name.all())
|
||||
return render(
|
||||
request,
|
||||
"shorts/question_findings.html",
|
||||
{
|
||||
"question": question,
|
||||
"question_finding_form": question_finding_form,
|
||||
"editing_finding": editing_finding,
|
||||
"can_edit": can_edit,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user