i should break these commits up
This commit is contained in:
+31
-7
@@ -2,6 +2,7 @@ from django import forms
|
|||||||
from django.contrib.admin import widgets
|
from django.contrib.admin import widgets
|
||||||
from django.forms import (
|
from django.forms import (
|
||||||
BaseInlineFormSet,
|
BaseInlineFormSet,
|
||||||
|
BooleanField,
|
||||||
Form,
|
Form,
|
||||||
HiddenInput,
|
HiddenInput,
|
||||||
ModelForm,
|
ModelForm,
|
||||||
@@ -54,6 +55,11 @@ import logging
|
|||||||
|
|
||||||
from generic.forms import ExamAuthorFormMixin
|
from generic.forms import ExamAuthorFormMixin
|
||||||
|
|
||||||
|
from crispy_forms.helper import FormHelper
|
||||||
|
|
||||||
|
from atlas.helpers import get_cases_available_to_user
|
||||||
|
|
||||||
|
|
||||||
class ConditionForm(ModelForm):
|
class ConditionForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Condition
|
model = Condition
|
||||||
@@ -75,7 +81,7 @@ class ConditionForm(ModelForm):
|
|||||||
class CaseCollectionForm(ModelForm):
|
class CaseCollectionForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CaseCollection
|
model = CaseCollection
|
||||||
exclude = ["cases", "valid_cid_users", "author"]
|
exclude = ["cases", "valid_cid_users", "valid_user_users", "author"]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.user = kwargs.pop(
|
self.user = kwargs.pop(
|
||||||
@@ -261,6 +267,7 @@ class CaseForm(ModelForm):
|
|||||||
|
|
||||||
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False))
|
# exams = ModelMultipleChoiceField(required=False, queryset=Exam.objects.all(),widget=FilteredSelectMultiple(verbose_name="Exams", is_stacked=False))
|
||||||
|
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
# Django also includes a few javascript files necessary
|
# Django also includes a few javascript files necessary
|
||||||
# for the operation of this form element. You need to
|
# for the operation of this form element. You need to
|
||||||
@@ -296,12 +303,18 @@ class CaseForm(ModelForm):
|
|||||||
# initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()]
|
# initial["exams"] = [t.pk for t in kwargs["instance"].exams.all()]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ModelForm.__init__(self, *args, **kwargs)
|
ModelForm.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
super(CaseForm, self).__init__(*args, **kwargs)
|
super(CaseForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# self.fields["series"].queryset =
|
# self.fields["series"].queryset =
|
||||||
|
|
||||||
|
if self.user.groups.filter(name="atlas_editor").exists():
|
||||||
|
case_queryset = Case.objects.all()
|
||||||
|
else:
|
||||||
|
case_queryset = get_cases_available_to_user(self.user)
|
||||||
|
self.fields["previous_case"].queryset=case_queryset
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return (
|
return (
|
||||||
super()
|
super()
|
||||||
@@ -401,6 +414,17 @@ CaseDifferentialFormSet = inlineformset_factory(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class ResourceForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Resource
|
||||||
|
exclude = ["author"]
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
"description": Textarea(attrs={"cols": 80, "rows": 5}),
|
||||||
|
"url": TextInput(attrs={"cols": 80, "rows": 1}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CaseResourceForm(ModelForm):
|
class CaseResourceForm(ModelForm):
|
||||||
|
|
||||||
def __init__(self, *args, user, **kwargs):
|
def __init__(self, *args, user, **kwargs):
|
||||||
@@ -417,6 +441,9 @@ class CaseResourceForm(ModelForm):
|
|||||||
# widget=Select(verbose_name="Series", is_stacked=False),
|
# widget=Select(verbose_name="Series", is_stacked=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.fields["pre_review"] = BooleanField(required=False)
|
||||||
|
|
||||||
|
|
||||||
class CaseSeriesForm(ModelForm):
|
class CaseSeriesForm(ModelForm):
|
||||||
|
|
||||||
def __init__(self, *args, user, **kwargs):
|
def __init__(self, *args, user, **kwargs):
|
||||||
@@ -440,10 +467,7 @@ class CaseCollectionCaseForm(ModelForm):
|
|||||||
def __init__(self, *args, user, **kwargs):
|
def __init__(self, *args, user, **kwargs):
|
||||||
super(CaseCollectionCaseForm, self).__init__(*args, **kwargs)
|
super(CaseCollectionCaseForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
if not user.groups.filter(name="atlas_editor").exists():
|
queryset = get_cases_available_to_user(user)
|
||||||
queryset = Case.objects.filter(author__id=user.id)
|
|
||||||
else:
|
|
||||||
queryset = Case.objects.all()
|
|
||||||
|
|
||||||
self.fields["case"] = ModelChoiceField(
|
self.fields["case"] = ModelChoiceField(
|
||||||
required=False,
|
required=False,
|
||||||
@@ -478,7 +502,7 @@ CaseResourceFormSet = inlineformset_factory(
|
|||||||
form=CaseResourceForm,
|
form=CaseResourceForm,
|
||||||
exclude=[],
|
exclude=[],
|
||||||
can_delete=True,
|
can_delete=True,
|
||||||
extra=0,
|
extra=1,
|
||||||
max_num=10,
|
max_num=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from atlas.models import Case
|
||||||
|
|
||||||
|
def get_cases_available_to_user(user):
|
||||||
|
"""Returns a queryset of cases available to the user"""
|
||||||
|
queryset = Case.objects.filter(author=user) | Case.objects.filter(open_access=True)
|
||||||
|
return queryset.order_by("pk")
|
||||||
|
if user.is_superuser:
|
||||||
|
return Case.objects.all()
|
||||||
|
else:
|
||||||
|
return Case.objects.filter(author=user) | Case.objects.filter(open_access=True)
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
# Generated by Django 5.0.2 on 2024-04-08 12:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0047_caseresource_pre_review_resource_author'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cidreportanswer',
|
||||||
|
name='completed',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userreportanswer',
|
||||||
|
name='completed',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='case',
|
||||||
|
name='condition',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='The condition(s) the case demonstrates.', to='atlas.condition'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='case',
|
||||||
|
name='diagnostic_certainty',
|
||||||
|
field=models.IntegerField(choices=[(0, 'None'), (1, 'Possible'), (2, 'Likely'), (3, 'Almost Certain'), (4, 'Certain')], default=0, help_text='The diagnostic certainty of the case.'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='case',
|
||||||
|
name='differential',
|
||||||
|
field=models.ManyToManyField(help_text='The differential diagnosis for the case.', related_name='casedifferential', through='atlas.Differential', to='atlas.condition'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='case',
|
||||||
|
name='presentation',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='The presentation(s) the case is associated with.', to='atlas.presentation'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='case',
|
||||||
|
name='previous_case',
|
||||||
|
field=models.OneToOneField(blank=True, help_text='If this case is related to another case on the system (e.g. follow up), link them here.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='next_case', to='atlas.case'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='case',
|
||||||
|
name='subspecialty',
|
||||||
|
field=models.ManyToManyField(blank=True, help_text='The subspecialties the case is associated with. Multiple subspecialties can be selected.', to='atlas.subspecialty'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='caseresource',
|
||||||
|
name='pre_review',
|
||||||
|
field=models.BooleanField(default=False, help_text='Defines if the resource should be shown pre review (i.e. for pre reading before the case is taken)'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+67
-33
@@ -311,20 +311,32 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
# findings = models.TextField(null=True, blank=True)
|
# findings = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
# subspecialty = models.CharField(max_length=2, choices=SubspecialtyChoices.choices)
|
# subspecialty = models.CharField(max_length=2, choices=SubspecialtyChoices.choices)
|
||||||
subspecialty = models.ManyToManyField(Subspecialty, blank=True)
|
subspecialty = models.ManyToManyField(
|
||||||
|
Subspecialty,
|
||||||
|
blank=True,
|
||||||
|
help_text="The subspecialties the case is associated with. Multiple subspecialties can be selected.",
|
||||||
|
)
|
||||||
|
|
||||||
condition = models.ManyToManyField(Condition, blank=True)
|
condition = models.ManyToManyField(
|
||||||
|
Condition, blank=True, help_text="The condition(s) the case demonstrates."
|
||||||
|
)
|
||||||
|
|
||||||
presentation = models.ManyToManyField(Presentation, blank=True)
|
presentation = models.ManyToManyField(
|
||||||
|
Presentation,
|
||||||
|
blank=True,
|
||||||
|
help_text="The presentation(s) the case is associated with.",
|
||||||
|
)
|
||||||
|
|
||||||
pathological_process = models.ManyToManyField(PathologicalProcess, blank=True)
|
pathological_process = models.ManyToManyField(PathologicalProcess, blank=True)
|
||||||
|
|
||||||
differential = models.ManyToManyField(
|
differential = models.ManyToManyField(
|
||||||
Condition, through=Differential, related_name="casedifferential"
|
Condition, through=Differential, related_name="casedifferential",
|
||||||
|
help_text="The differential diagnosis for the case.",
|
||||||
)
|
)
|
||||||
|
|
||||||
diagnostic_certainty = models.IntegerField(
|
diagnostic_certainty = models.IntegerField(
|
||||||
choices=CertaintyChoices.choices, default=CertaintyChoices.NONE
|
choices=CertaintyChoices.choices, default=CertaintyChoices.NONE,
|
||||||
|
help_text="The diagnostic certainty of the case.",
|
||||||
)
|
)
|
||||||
|
|
||||||
verified = models.BooleanField(default=False)
|
verified = models.BooleanField(default=False)
|
||||||
@@ -352,7 +364,9 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
help_text="If a case should be freely available to browse", default=True
|
help_text="If a case should be freely available to browse", default=True
|
||||||
)
|
)
|
||||||
|
|
||||||
series = models.ManyToManyField("Series", through="SeriesDetail", related_name="case")
|
series = models.ManyToManyField(
|
||||||
|
"Series", through="SeriesDetail", related_name="case"
|
||||||
|
)
|
||||||
|
|
||||||
notes = GenericRelation(QuestionNote)
|
notes = GenericRelation(QuestionNote)
|
||||||
|
|
||||||
@@ -362,6 +376,7 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
null=True,
|
null=True,
|
||||||
related_name="next_case",
|
related_name="next_case",
|
||||||
blank=True,
|
blank=True,
|
||||||
|
help_text="If this case is related to another case on the system (e.g. follow up), link them here.",
|
||||||
)
|
)
|
||||||
|
|
||||||
resource = models.ManyToManyField("Resource", through="CaseResource")
|
resource = models.ManyToManyField("Resource", through="CaseResource")
|
||||||
@@ -378,12 +393,15 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
# return f"{self.pk}: {self.title}"
|
# return f"{self.pk}: {self.title}"
|
||||||
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), str(self))
|
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), str(self))
|
||||||
|
|
||||||
#def get_base_template(self):
|
# def get_base_template(self):
|
||||||
|
|
||||||
#def get_edit_template_links(self):
|
# def get_edit_template_links(self):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.pk}: {self.title}"
|
if self.open_access:
|
||||||
|
return f"{self.pk}: {self.title} [OA]"
|
||||||
|
else:
|
||||||
|
return f"{self.pk}: {self.title}"
|
||||||
|
|
||||||
def get_series_blocks(self):
|
def get_series_blocks(self):
|
||||||
html = ""
|
html = ""
|
||||||
@@ -396,7 +414,7 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
examinations = [series.get_examination() for series in self.series.all()]
|
examinations = [series.get_examination() for series in self.series.all()]
|
||||||
return f"{self.pk}:{self.title} {'/'.join([str(c) for c in self.condition.all()])} [{', '.join(examinations)}]"
|
return f"{self.pk}:{self.title} {'/'.join([str(c) for c in self.condition.all()])} [{', '.join(examinations)}]"
|
||||||
|
|
||||||
def get_case_dicom_json(self, case_title_as_patient_name = True):
|
def get_case_dicom_json(self, case_title_as_patient_name=True):
|
||||||
series_json = []
|
series_json = []
|
||||||
for series in self.series.all():
|
for series in self.series.all():
|
||||||
series_json.append(series.get_series_dicom_json())
|
series_json.append(series.get_series_dicom_json())
|
||||||
@@ -435,7 +453,6 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
return json.dumps(self.get_viva_details())
|
return json.dumps(self.get_viva_details())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def extract_image_dicom_json_from_ds(ds, url, image_index):
|
def extract_image_dicom_json_from_ds(ds, url, image_index):
|
||||||
to_keep = [
|
to_keep = [
|
||||||
"Columns",
|
"Columns",
|
||||||
@@ -597,7 +614,7 @@ class Series(SeriesBase):
|
|||||||
"<a href='{}'>{}</a>", self.get_absolute_url(), self.get_full_str()
|
"<a href='{}'>{}</a>", self.get_absolute_url(), self.get_full_str()
|
||||||
)
|
)
|
||||||
|
|
||||||
#@lru_cache(maxsize=128)
|
# @lru_cache(maxsize=128)
|
||||||
def get_series_dicom_json(self):
|
def get_series_dicom_json(self):
|
||||||
instances = []
|
instances = []
|
||||||
|
|
||||||
@@ -662,12 +679,13 @@ class Series(SeriesBase):
|
|||||||
image = self.images.first()
|
image = self.images.first()
|
||||||
|
|
||||||
with pydicom.dcmread(image.image) as ds:
|
with pydicom.dcmread(image.image) as ds:
|
||||||
date = ds.get("StudyDate", "No date")
|
date = ds.get("StudyDate", "No date")
|
||||||
|
|
||||||
self.description = f"{date[:4]}-{date[4:6]}-{date[6:]}"
|
self.description = f"{date[:4]}-{date[4:6]}-{date[6:]}"
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
class CaseCollection(ExamOrCollectionGenericBase):
|
class CaseCollection(ExamOrCollectionGenericBase):
|
||||||
app_name = "atlas"
|
app_name = "atlas"
|
||||||
|
|
||||||
@@ -806,9 +824,13 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
|||||||
self.VIEWER_MODE_CHOICES.BUILT_IN_WITH_OHIF,
|
self.VIEWER_MODE_CHOICES.BUILT_IN_WITH_OHIF,
|
||||||
)
|
)
|
||||||
|
|
||||||
def show_ohif_viewer_link(self) -> bool:
|
def show_ohif_viewer(self) -> bool:
|
||||||
return self.viewer_mode in (
|
return self.viewer_mode in (
|
||||||
self.VIEWER_MODE_CHOICES.OHIF,
|
self.VIEWER_MODE_CHOICES.OHIF,
|
||||||
|
)
|
||||||
|
|
||||||
|
def show_ohif_viewer_link(self) -> bool:
|
||||||
|
return self.viewer_mode in (
|
||||||
self.VIEWER_MODE_CHOICES.BUILT_IN_WITH_OHIF,
|
self.VIEWER_MODE_CHOICES.BUILT_IN_WITH_OHIF,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -843,9 +865,7 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
|||||||
) -> Case | Tuple[Case, int]:
|
) -> Case | Tuple[Case, int]:
|
||||||
# Seems to be a bug when case_number is 0 or 1 the same (first) object gets returned
|
# Seems to be a bug when case_number is 0 or 1 the same (first) object gets returned
|
||||||
# forces a list fixes (but is likely inefficient)
|
# forces a list fixes (but is likely inefficient)
|
||||||
cases = list(
|
cases = list(self.get_cases().prefetch_related())
|
||||||
self.get_cases().prefetch_related()
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
case = cases[case_index]
|
case = cases[case_index]
|
||||||
@@ -859,16 +879,14 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
|||||||
return case
|
return case
|
||||||
|
|
||||||
def get_case_take_url(self, case):
|
def get_case_take_url(self, case):
|
||||||
cases = list(
|
cases = list(self.get_cases().prefetch_related())
|
||||||
self.get_cases().prefetch_related()
|
|
||||||
)
|
|
||||||
|
|
||||||
return reverse(
|
return reverse(
|
||||||
"atlas:collection_case_view_take_user",
|
"atlas:collection_case_view_take_user",
|
||||||
kwargs={"pk": self.pk, "case_number": cases.index(case)},
|
kwargs={"pk": self.pk, "case_number": cases.index(case)},
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_ohif_dicom_json(self, case_title_as_patient_name = True):
|
def get_ohif_dicom_json(self, case_title_as_patient_name=True):
|
||||||
studies = []
|
studies = []
|
||||||
for n, case in enumerate(self.cases.all()):
|
for n, case in enumerate(self.cases.all()):
|
||||||
series_json = []
|
series_json = []
|
||||||
@@ -880,7 +898,6 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
|||||||
patient_name = case.title
|
patient_name = case.title
|
||||||
|
|
||||||
studies.append(
|
studies.append(
|
||||||
|
|
||||||
{
|
{
|
||||||
"StudyInstanceUID": f"1.3.6.1.4.1.14519.5.2.1.6279.6001.{n}",
|
"StudyInstanceUID": f"1.3.6.1.4.1.14519.5.2.1.6279.6001.{n}",
|
||||||
"StudyDate": "20000101",
|
"StudyDate": "20000101",
|
||||||
@@ -894,21 +911,16 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return {"studies": studies}
|
||||||
|
|
||||||
return {
|
|
||||||
"studies": studies
|
|
||||||
}
|
|
||||||
|
|
||||||
def add_case(self, case):
|
def add_case(self, case):
|
||||||
"""Adds a case to the collection and makes sure order is maintained"""
|
"""Adds a case to the collection and makes sure order is maintained"""
|
||||||
# We might be better off adding via the case detail model direct
|
# We might be better off adding via the case detail model direct
|
||||||
#CaseDetail.objects.create(case=case, collection=self)
|
# CaseDetail.objects.create(case=case, collection=self)
|
||||||
self.cases.add(case)
|
self.cases.add(case)
|
||||||
self.order_cases()
|
self.order_cases()
|
||||||
|
|
||||||
|
def order_cases(self):
|
||||||
def order_cases(self):
|
|
||||||
"""Modifies the casedetail sort_order to sequentially order the cases"""
|
"""Modifies the casedetail sort_order to sequentially order the cases"""
|
||||||
for n, c in enumerate(self.casedetail_set.all().order_by("sort_order")):
|
for n, c in enumerate(self.casedetail_set.all().order_by("sort_order")):
|
||||||
c.sort_order = n
|
c.sort_order = n
|
||||||
@@ -921,11 +933,15 @@ class SeriesDetail(models.Model):
|
|||||||
|
|
||||||
sort_order = models.IntegerField(default=1000)
|
sort_order = models.IntegerField(default=1000)
|
||||||
|
|
||||||
feedback = models.BooleanField(default=False, help_text="Set to true if the series should only be shown for feedback purposes.")
|
feedback = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Set to true if the series should only be shown for feedback purposes.",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("sort_order",)
|
ordering = ("sort_order",)
|
||||||
|
|
||||||
|
|
||||||
class CaseDetail(models.Model):
|
class CaseDetail(models.Model):
|
||||||
case = models.ForeignKey(Case, on_delete=models.CASCADE)
|
case = models.ForeignKey(Case, on_delete=models.CASCADE)
|
||||||
collection = models.ForeignKey(CaseCollection, on_delete=models.CASCADE)
|
collection = models.ForeignKey(CaseCollection, on_delete=models.CASCADE)
|
||||||
@@ -950,6 +966,8 @@ class BaseReportAnswer(models.Model):
|
|||||||
blank=True, null=True, validators=[MaxValueValidator(10), MinValueValidator(0)]
|
blank=True, null=True, validators=[MaxValueValidator(10), MinValueValidator(0)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
completed = models.BooleanField(default=False)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
self.clean()
|
self.clean()
|
||||||
return super(BaseReportAnswer, self).save(*args, **kwargs)
|
return super(BaseReportAnswer, self).save(*args, **kwargs)
|
||||||
@@ -1145,6 +1163,7 @@ class UncategorisedDicom(models.Model):
|
|||||||
class DuplicateDicom(Exception):
|
class DuplicateDicom(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Resource(models.Model, AuthorMixin):
|
class Resource(models.Model, AuthorMixin):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
@@ -1166,11 +1185,26 @@ class Resource(models.Model, AuthorMixin):
|
|||||||
def get_link(self):
|
def get_link(self):
|
||||||
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), self.name)
|
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), self.name)
|
||||||
|
|
||||||
|
def get_display(self) -> str:
|
||||||
|
if self.url:
|
||||||
|
html = f"<a target='_blank' href='{self.url}'>{self.name}</a>"
|
||||||
|
elif self.file:
|
||||||
|
html = f"<a target='_blank' href='{self.file.url}'>{self.name}</a>"
|
||||||
|
else:
|
||||||
|
html = self.name
|
||||||
|
|
||||||
|
return format_html(html)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CaseResource(models.Model):
|
class CaseResource(models.Model):
|
||||||
resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
|
resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
|
||||||
case = models.ForeignKey(Case, on_delete=models.CASCADE)
|
case = models.ForeignKey(Case, on_delete=models.CASCADE)
|
||||||
|
|
||||||
pre_review = models.BooleanField(default=False)
|
pre_review = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Defines if the resource should be shown pre review (i.e. for pre reading before the case is taken)",
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.resource} - {self.case}"
|
return f"{self.resource} - {self.case} (Pre: {self.pre_review})"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<span id="add-case-form">
|
<span id="add-case-form">
|
||||||
<form hx-post="{% url 'atlas:add_case_to_collection' collection.id %}" hx-swap="outerHTML">
|
<form hx-post="{% url 'atlas:add_case_to_collection' collection.id %}" hx-swap="outerHTML">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label for="case">Select Case:</label>
|
<label for="case">Select Case to add:</label>
|
||||||
<select name="case" id="case">
|
<select name="case" id="case">
|
||||||
{% for case in cases %}
|
{% for case in cases %}
|
||||||
<option value="{{ case.id }}">{{ case.title }}</option>
|
<option value="{{ case.id }}">{{ case.title }}</option>
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
<a href="{% url 'atlas:case_view' %}?sort=-created_date">Cases</a> /
|
<a href="{% url 'atlas:case_view' %}?sort=-created_date">Cases</a> /
|
||||||
<a href="{% url 'atlas:series_view' %}">Series</a> /
|
<a href="{% url 'atlas:series_view' %}">Series</a> /
|
||||||
<a href="{% url 'atlas:categories_list' %}">Categories</a> /
|
<a href="{% url 'atlas:categories_list' %}">Categories</a> /
|
||||||
|
{% comment %} <a href="{% url 'atlas:resource_view' %}">Resources</a> / {% endcomment %}
|
||||||
<a href="{% url 'atlas:case_create' %}" title="Create a new atlas case">Create Case</a> /
|
<a href="{% url 'atlas:case_create' %}" title="Create a new atlas case">Create Case</a> /
|
||||||
<a href="{% url 'atlas:series_create' %}" title="Create a new image series">Create Series</a> /
|
<a href="{% url 'atlas:series_create' %}" title="Create a new image series">Create Series</a> /
|
||||||
|
<a href="{% url 'atlas:resource_view' %}" title="Resources">Resources</a> /
|
||||||
<a href="{% url 'atlas:user_uploads' %}" title="View unimported uploads">Uploads</a> /
|
<a href="{% url 'atlas:user_uploads' %}" title="View unimported uploads">Uploads</a> /
|
||||||
<a href="{% url 'atlas:help' %}" title="View the atlas help pages">Help</a>
|
<a href="{% url 'atlas:help' %}" title="View the atlas help pages">Help</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,182 +1,190 @@
|
|||||||
<div class="atlas {% if case.scrapped %}atlas-scrapped{% endif %}">
|
<div class="atlas {% if case.scrapped %}atlas-scrapped{% endif %}">
|
||||||
<div><a href="https://viewer.penracourses.org.uk/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">View case in OHIF</a> <a target="_blank" href="https://viewer.penracourses.org.uk/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">(new tab)</a></div>
|
<div><a href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">View case in OHIF</a> <a target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">(new tab)</a>
|
||||||
<div><a href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">View case in OHIF</a>
|
|
||||||
|
|
||||||
<div class="date">
|
<div class="date">
|
||||||
{{ case.created_date|date:"d/m/Y" }}
|
{{ case.created_date|date:"d/m/Y" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="id">
|
<div class="id">
|
||||||
ID: {{ case.id }}
|
ID: {{ case.id }}
|
||||||
</div>
|
</div>
|
||||||
<p class="pre-whitespace"><b>Title:</b> {{ case.title }}</p>
|
<p class="pre-whitespace"><b>Title:</b> {{ case.title }}</p>
|
||||||
|
|
||||||
<p class="pre-whitespace"><b>Description:</b> {{ case.description }}</p>
|
<p class="pre-whitespace"><b>Description:</b> {{ case.description }}</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<b>Presentation:</b>
|
<b>Presentation:</b>
|
||||||
<ul>
|
<ul>
|
||||||
{% for presentation in case.presentation.all %}
|
{% for presentation in case.presentation.all %}
|
||||||
<li>{{presentation.get_link}}</li>
|
<li>{{presentation.get_link}}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="pre-whitespace"><b>History:</b> {{ case.history }}</p>
|
<p class="pre-whitespace"><b>History:</b> {{ case.history }}</p>
|
||||||
|
|
||||||
<p class="pre-whitespace"><b>Discussion:</b> {{ case.discussion }}</p>
|
<p class="pre-whitespace"><b>Discussion:</b> {{ case.discussion }}</p>
|
||||||
|
|
||||||
<p class="pre-whitespace"><b>Report:</b> {{ case.report }}</p>
|
<p class="pre-whitespace"><b>Report:</b> {{ case.report }}</p>
|
||||||
|
<p class="pre-whitespace"><b>Resource</b><br/>{% for caseresource in case.caseresource_set.all %}
|
||||||
<div class="pre-whitespace multi-image-block"><b>Series:</b>
|
<a href='{% url "atlas:resource_detail" caseresource.resource.pk %}'>{{caseresource.resource.name}}</a>
|
||||||
<form>
|
{% if caseresource.pre_review %}
|
||||||
{% for series in case.series.all %}
|
<span title="This resource will be avalible to review when or taking a case">(pre)</span>
|
||||||
<span class="series-block">
|
{% else %}
|
||||||
<span>
|
<span title="This resource will only be available when reviewing a completed case">(post)</span>
|
||||||
<span class="series-block-series-number">Series {{ forloop.counter }}:</span><br>
|
{% endif %}
|
||||||
<a href="{% url 'atlas:series_detail' pk=series.pk %}">
|
|
||||||
{{series.get_block}}
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<input type="checkbox" name="series-ids" value="{{series.pk}}">
|
|
||||||
<span class="series-block-popup-link">
|
|
||||||
<a href="#"
|
|
||||||
onclick="return window.create_popup_window('/atlas/series/{{series.pk}}', 'Series')">Popup</a>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</form>
|
</p>
|
||||||
<span>
|
|
||||||
<a href="{% url 'atlas:series_id_create' pk=case.pk %}">Add new series</a><br />
|
|
||||||
<a href="{% url 'atlas:user_uploads_case' case_id=case.pk %}">Import new uploads</a><br />
|
|
||||||
<details>
|
|
||||||
<summary>+</summary>
|
|
||||||
<button hx-get="{% url 'atlas:case_order_dicom' pk=case.pk %}"
|
|
||||||
title="order dicom by slice location"
|
|
||||||
hx-target="this"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-confirm="This will reorder all case series based upon slice location"
|
|
||||||
>
|
|
||||||
Order all series dicoms by slice location
|
|
||||||
</button>
|
|
||||||
<button hx-post="{% url 'atlas:combine_series' %}"
|
|
||||||
title="merge series"
|
|
||||||
hx-include="[name='series-ids']"
|
|
||||||
hx-target="#series-merge-results"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-confirm="This will merge all selected series into one"
|
|
||||||
>
|
|
||||||
Merge selected series
|
|
||||||
</button>
|
|
||||||
<button hx-post="{% url 'atlas:use_dates_as_descriptions' case.pk %}"
|
|
||||||
title="merge series"
|
|
||||||
hx-include="[name='series-ids']"
|
|
||||||
hx-target="#series-merge-results"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-confirm="This will use the dicom date as the series description"
|
|
||||||
>
|
|
||||||
Use dates as description
|
|
||||||
</button>
|
|
||||||
<br/>
|
|
||||||
<span id="series-merge-results"></span>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
</span>
|
<div class="pre-whitespace multi-image-block"><b>Series:</b>
|
||||||
</div>
|
<form>
|
||||||
|
{% for series in case.series.all %}
|
||||||
|
<span class="series-block">
|
||||||
|
<span>
|
||||||
|
<span class="series-block-series-number">Series {{ forloop.counter }}:</span><br>
|
||||||
|
<a href="{% url 'atlas:series_detail' pk=series.pk %}">
|
||||||
|
{{series.get_block}}
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<input type="checkbox" name="series-ids" value="{{series.pk}}">
|
||||||
|
<span class="series-block-popup-link">
|
||||||
|
<a href="#"
|
||||||
|
onclick="return window.create_popup_window('/atlas/series/{{series.pk}}', 'Series')">Popup</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</form>
|
||||||
|
<span>
|
||||||
|
<a href="{% url 'atlas:series_id_create' pk=case.pk %}">Add new series</a><br />
|
||||||
|
<a href="{% url 'atlas:user_uploads_case' case_id=case.pk %}">Import new uploads</a><br />
|
||||||
|
<details>
|
||||||
|
<summary>+</summary>
|
||||||
|
<button hx-get="{% url 'atlas:case_order_dicom' pk=case.pk %}"
|
||||||
|
title="order dicom by slice location"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-confirm="This will reorder all case series based upon slice location"
|
||||||
|
>
|
||||||
|
Order all series dicoms by slice location
|
||||||
|
</button>
|
||||||
|
<button hx-post="{% url 'atlas:combine_series' %}"
|
||||||
|
title="merge series"
|
||||||
|
hx-include="[name='series-ids']"
|
||||||
|
hx-target="#series-merge-results"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-confirm="This will merge all selected series into one"
|
||||||
|
>
|
||||||
|
Merge selected series
|
||||||
|
</button>
|
||||||
|
<button hx-post="{% url 'atlas:use_dates_as_descriptions' case.pk %}"
|
||||||
|
title="merge series"
|
||||||
|
hx-include="[name='series-ids']"
|
||||||
|
hx-target="#series-merge-results"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-confirm="This will use the dicom date as the series description"
|
||||||
|
>
|
||||||
|
Use dates as description
|
||||||
|
</button>
|
||||||
|
<br/>
|
||||||
|
<span id="series-merge-results"></span>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{% comment %} <p><b>Scrapped:</b> {{ case.scrapped }} <a
|
{% comment %} <p><b>Scrapped:</b> {{ case.scrapped }} <a
|
||||||
href="{% url 'atlas:case_scrap' pk=case.pk %}">(toggle)</a> {% endcomment %}
|
href="{% url 'atlas:case_scrap' pk=case.pk %}">(toggle)</a> {% endcomment %}
|
||||||
|
|
||||||
<div class="atlas-answer">
|
<div class="atlas-answer">
|
||||||
<p class="pre-whitespace">
|
<p class="pre-whitespace">
|
||||||
Condition:
|
Condition:
|
||||||
<ul>
|
<ul>
|
||||||
{% for con in case.condition.all %}
|
{% for con in case.condition.all %}
|
||||||
<li>{{con.get_link}}</li>
|
<li>{{con.get_link}}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<div>
|
<div>
|
||||||
<details>
|
<details>
|
||||||
<summary><b>Findings</b></summary>
|
<summary><b>Findings</b></summary>
|
||||||
{% if case.series.all %}
|
{% if case.series.all %}
|
||||||
{% for series in case.series.all %}
|
{% for series in case.series.all %}
|
||||||
{% for finding in series.findings.all %}
|
{% for finding in series.findings.all %}
|
||||||
<div class="finding-box">
|
<div class="finding-box">
|
||||||
<span>
|
|
||||||
<a href="{{series.get_absolute_url}}">Series {{series.pk}}</a>: {{series.get_examination_full}} |
|
|
||||||
</span>
|
|
||||||
{% if finding.findings.all %}
|
|
||||||
<span>
|
<span>
|
||||||
Findings: {% for f in finding.findings.all %}
|
<a href="{{series.get_absolute_url}}">Series {{series.pk}}</a>: {{series.get_examination_full}} |
|
||||||
{{f.get_link}},
|
|
||||||
{% endfor %}
|
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% if finding.findings.all %}
|
||||||
{% if finding.structures.all %}
|
<span>
|
||||||
<span>
|
Findings: {% for f in finding.findings.all %}
|
||||||
Structure: {% for s in finding.structures.all %}
|
{{f.get_link}},
|
||||||
{{s.get_link}},
|
{% endfor %}
|
||||||
{% endfor %}
|
</span>
|
||||||
</span>
|
{% endif %}
|
||||||
{% endif %}
|
{% if finding.structures.all %}
|
||||||
{% if finding.description %}
|
<span>
|
||||||
<span>
|
Structure: {% for s in finding.structures.all %}
|
||||||
Description: {{finding.description}}
|
{{s.get_link}},
|
||||||
</span>
|
{% endfor %}
|
||||||
{% endif %}
|
</span>
|
||||||
</div>
|
{% endif %}
|
||||||
|
{% if finding.description %}
|
||||||
|
<span>
|
||||||
|
Description: {{finding.description}}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% else %}
|
||||||
{% else %}
|
No series associated with case.
|
||||||
No series associated with case.
|
{% endif %}
|
||||||
{% endif %}
|
</details>
|
||||||
</details>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<details open>
|
||||||
<details open>
|
<summary><b>Differentials</b></summary>
|
||||||
<summary><b>Differentials</b></summary>
|
<ul>
|
||||||
<ul>
|
{% for diff in case.differentialcase.all %}
|
||||||
{% for diff in case.differentialcase.all %}
|
<li>{{diff.condition.get_link}} - {{diff.text}}</li>
|
||||||
<li>{{diff.condition.get_link}} - {{diff.text}}</li>
|
{% endfor %}
|
||||||
{% endfor %}
|
</ul>
|
||||||
</ul>
|
</details>
|
||||||
</details>
|
</div>
|
||||||
</div>
|
</p>
|
||||||
</p>
|
</div>
|
||||||
|
Subspecialty:
|
||||||
|
<ul>
|
||||||
|
{% for sub in case.subspecialty.all %}
|
||||||
|
<li>{{sub.get_link}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<b>Pathological Process:</b>
|
||||||
|
<ul>
|
||||||
|
{% for p in case.pathological_process.all %}
|
||||||
|
<li>{{p.get_link}}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<b>Diagnostic certainty:</b> {{case.get_diagnostic_certainty_display}}<br />
|
||||||
|
<b>Collections:</b>
|
||||||
|
<button
|
||||||
|
hx-get="{% url 'atlas:case_collection_form' case.pk %}"
|
||||||
|
hx-target="#collection-form">
|
||||||
|
Add collection</button>
|
||||||
|
<div id="collection-form"></div>
|
||||||
|
<ul>
|
||||||
|
{% for collection in case.casecollection_set.all %}
|
||||||
|
<li><a href="{% url 'atlas:collection_detail' pk=collection.pk %}">{{collection.name}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<p class="pre-whitespace"><b>Previous case:</b> {{ case.previous_case.get_link }}</p>
|
||||||
|
<p class="pre-whitespace"><b>Next case:</b> {{ case.next_case.get_link }}</p>
|
||||||
|
<div>
|
||||||
|
{% include 'question_notes.html' %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
Subspecialty:
|
|
||||||
<ul>
|
|
||||||
{% for sub in case.subspecialty.all %}
|
|
||||||
<li>{{sub.get_link}}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<b>Pathological Process:</b>
|
|
||||||
<ul>
|
|
||||||
{% for p in case.pathological_process.all %}
|
|
||||||
<li>{{p.get_link}}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<b>Diagnostic certainty:</b> {{case.get_diagnostic_certainty_display}}<br />
|
|
||||||
<b>Collections:</b>
|
|
||||||
<button
|
|
||||||
hx-get="{% url 'atlas:case_collection_form' case.pk %}"
|
|
||||||
hx-target="#collection-form">
|
|
||||||
Add collection</button>
|
|
||||||
<div id="collection-form"></div>
|
|
||||||
<ul>
|
|
||||||
{% for collection in case.casecollection_set.all %}
|
|
||||||
<li><a href="{% url 'atlas:collection_detail' pk=collection.pk %}">{{collection.name}}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</p>
|
|
||||||
<p class="pre-whitespace"><b>Previous case:</b> {{ case.previous_case.get_link }}</p>
|
|
||||||
<p class="pre-whitespace"><b>Next case:</b> {{ case.next_case.get_link }}</p>
|
|
||||||
<div>
|
|
||||||
{% include 'question_notes.html' %}
|
|
||||||
|
|
||||||
</div>
|
<p><b>Checked by:</b> {% for verified in case.verified.all %} <a
|
||||||
|
href="{% url 'atlas:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
||||||
|
|
||||||
<p><b>Checked by:</b> {% for verified in case.verified.all %} <a
|
<p><b>Author(s):</b> {% for author in case.author.all %} <a
|
||||||
href="{% url 'atlas:verified_detail' pk=verified.pk %}">{{verified}}</a>, {% endfor %}</p>
|
href="{% url 'atlas:author_detail' pk=author.pk %}">{{author}}</a>, {% endfor %}</p>
|
||||||
|
|
||||||
<p><b>Author(s):</b> {% for author in case.author.all %} <a
|
|
||||||
href="{% url 'atlas:author_detail' pk=author.pk %}">{{author}}</a>, {% endfor %}</p>
|
|
||||||
|
|||||||
@@ -29,18 +29,18 @@
|
|||||||
|
|
||||||
function add_series_input_form() {
|
function add_series_input_form() {
|
||||||
var form_idx = $('#id_Case_series-TOTAL_FORMS').val();
|
var form_idx = $('#id_Case_series-TOTAL_FORMS').val();
|
||||||
$('#series_formset').append($('#empty_series_form').html().replace(/__prefix__/g, form_idx));
|
$('#series_formset ol').append($('#empty_series_form').html().replace(/__prefix__/g, form_idx));
|
||||||
$('#id_Case_series-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
$('#id_Case_series-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function add_differential_input_form() {
|
function add_differential_input_form() {
|
||||||
var form_idx = $('#id_differentialcase-TOTAL_FORMS').val();
|
var form_idx = $('#id_differentialcase-TOTAL_FORMS').val();
|
||||||
$('#casedifferential_formset').append($('#empty_casedifferential_form').html().replace(/__prefix__/g, form_idx));
|
$('#casedifferential_formset ol').append($('#empty_casedifferential_form').html().replace(/__prefix__/g, form_idx));
|
||||||
$('#id_differentialcase-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
$('#id_differentialcase-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
||||||
}
|
}
|
||||||
function add_resource_input_form() {
|
function add_resource_input_form() {
|
||||||
var form_idx = $('#id_resourcecase-TOTAL_FORMS').val();
|
var form_idx = $('#id_resourcecase-TOTAL_FORMS').val();
|
||||||
$('#caseresource_formset').append($('#empty_caseresource_form').html().replace(/__prefix__/g, form_idx));
|
$('#caseresource_formset ol').append($('#empty_caseresource_form').html().replace(/__prefix__/g, form_idx));
|
||||||
$('#id_resourcecase-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
$('#id_resourcecase-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,72 +60,82 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2>Submit Case</h2>
|
<h2>Submit Case</h2>
|
||||||
Use this form to create a atlas case. Existing associated image sets can be added using this form.
|
<details>
|
||||||
|
<summary>Instructions</summary>
|
||||||
|
<p>Use this form to create a atlas case</p>
|
||||||
|
<p>Existing associated image sets can be added using this form.</p>
|
||||||
|
<p>The more information you provide, the more useful the case will be to others.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
{% if form.collection %}
|
{% if form.collection %}
|
||||||
<div class="alert alert-info" role="alert">Creating a case in the collection: <a href="{% url 'atlas:collection_detail' pk=form.collection.pk %}">{{form.collection.name}}</a></div>
|
<div class="alert alert-info" role="alert">Creating a case in the collection: <a href="{% url 'atlas:collection_detail' pk=form.collection.pk %}">{{form.collection.name}}</a></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form action="" method="post" enctype="multipart/form-data" id="atlas-form">
|
<form action="" method="post" enctype="multipart/form-data" id="atlas-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<table>
|
{{ form }}
|
||||||
{{ form.as_table }}
|
|
||||||
</table>
|
|
||||||
<h3>Series:</h3>
|
<h3>Series:</h3>
|
||||||
Add image sets here. These can only be added once created (they can also be added to cases on creation).
|
Add image sets here. These can only be added once created (they can also be added to cases on creation).
|
||||||
<input type="button" value="Add More Series" id="add_more_series">
|
<input type="button" value="Add More Series" id="add_more_series">
|
||||||
<div id="series_formset">
|
<div id="series_formset" class="list_formset">
|
||||||
|
<ol>
|
||||||
{% for form in series_formset %}
|
{% for form in series_formset %}
|
||||||
<ul class="no-error series-formset">
|
<li class="no-error series-formset">
|
||||||
{{form.non_field_errors}}
|
{{form.non_field_errors}}
|
||||||
{{form.errors}}
|
{{form.errors}}
|
||||||
{{ form.as_ul }}
|
{{ form }}
|
||||||
</ul>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
{{ series_formset.management_form }}
|
{{ series_formset.management_form }}
|
||||||
<h3>Differential:</h3>
|
<h3>Differential:</h3>
|
||||||
Add differential here.
|
Add differential here.
|
||||||
<input type="button" value="Add Another Differential" id="add_more_differential">
|
<input type="button" value="Add Another Differential" id="add_more_differential">
|
||||||
<div id="casedifferential_formset">
|
<div id="casedifferential_formset" class="list_formset">
|
||||||
|
<ol>
|
||||||
{% for form in casedifferential_formset %}
|
{% for form in casedifferential_formset %}
|
||||||
<ul class="no-error casedifferential_formset">
|
<li class="no-error casedifferential_formset">
|
||||||
{{form.non_field_errors}}
|
{{form.non_field_errors}}
|
||||||
{{form.errors}}
|
{{form.errors}}
|
||||||
{{ form.as_ul }}
|
{{ form }}
|
||||||
</ul>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
{{ casedifferential_formset.management_form }}
|
{{ casedifferential_formset.management_form }}
|
||||||
<h3>Resource:</h3>
|
<h3>Resource:</h3>
|
||||||
Add resource here.
|
Add resource here. Resource can be anything that is useful for the case (videos, pdf, etc). This can either be avalaible before attempting the case (such a for a pre reading list) or after.
|
||||||
<input type="button" value="Add Another Resource" id="add_more_resource">
|
<input type="button" value="Add Another Resource" id="add_more_resource">
|
||||||
<div id="caseresource_formset">
|
<div id="caseresource_formset" class="list_formset">
|
||||||
|
<ol>
|
||||||
{% for form in caseresource_formset %}
|
{% for form in caseresource_formset %}
|
||||||
<ul class="no-error caseresource_formset">
|
<li class="no-error caseresource_formset">
|
||||||
{{form.non_field_errors}}
|
{{form.non_field_errors}}
|
||||||
{{form.errors}}
|
{{form.errors}}
|
||||||
{{ form.as_ul }}
|
{{ form }}
|
||||||
</ul>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
{{ caseresource_formset.management_form }}
|
{{ caseresource_formset.management_form }}
|
||||||
<br/>
|
<br/>
|
||||||
<input type="submit" class="submit-button" value="Submit" name="submit">
|
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||||
</form>
|
</form>
|
||||||
<div id="empty_series_form" style="display:none">
|
<div id="empty_series_form" style="display:none">
|
||||||
<ul class='no_error series-formset'>
|
<li class='no_error series-formset'>
|
||||||
{{ series_formset.empty_form.as_ul }}
|
{{ series_formset.empty_form }}
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
<div id="empty_casedifferential_form" style="display:none">
|
<div id="empty_casedifferential_form" style="display:none">
|
||||||
<ul class='no_error casedifferential_formset'>
|
<li class='no_error casedifferential_formset'>
|
||||||
{{ casedifferential_formset.empty_form.as_ul }}
|
{{ casedifferential_formset.empty_form }}
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
<div id="empty_caseresource_form" style="display:none">
|
<div id="empty_caseresource_form" style="display:none">
|
||||||
<ul class='no_error caseresource_formset'>
|
<li class='no_error caseresource_formset'>
|
||||||
{{ caseresource_formset.empty_form.as_ul }}
|
{{ caseresource_formset.empty_form }}
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% extends "atlas/base.html" %}
|
{% extends "atlas/exams.html" %}
|
||||||
<!-- {% load static from static %} -->
|
<!-- {% load static from static %} -->
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
function add_case_input_form() {
|
function add_case_input_form() {
|
||||||
var form_idx = $('#id_casedetail_set-TOTAL_FORMS').val();
|
var form_idx = $('#id_casedetail_set-TOTAL_FORMS').val();
|
||||||
$('#case_formset').append($('#empty_case_form').html().replace(/__prefix__/g, form_idx));
|
$('#case_formset ol').append($('#empty_case_form').html().replace(/__prefix__/g, form_idx));
|
||||||
$('#id_casedetail_set-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
$('#id_casedetail_set-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,34 +23,43 @@
|
|||||||
<!-- {{ form.media }} -->
|
<!-- {{ form.media }} -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
{% if collection %}
|
||||||
|
<h2>Update Collection</h2>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
<h2>Create Collection</h2>
|
<h2>Create Collection</h2>
|
||||||
Use this form to create a collection of cases
|
Use this form to create a collection of cases
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form action="" method="post" enctype="multipart/form-data" id="atlas-form">
|
<form action="" method="post" enctype="multipart/form-data" id="atlas-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<table>
|
{{ form }}
|
||||||
{{ form.as_table }}
|
|
||||||
</table>
|
|
||||||
<h3>Cases:</h3>
|
<h3>Cases:</h3>
|
||||||
Add cases here. These can only be added once created (they can also be added to cases on creation). Click and drag to change order.
|
Add cases here. These can only be added once created (they can also be added to cases on creation). Click and drag to change order.
|
||||||
<input type="button" value="Add More Cases" id="add_more_case">
|
<input type="button" value="Add More Cases" id="add_more_case">
|
||||||
<input type=button id="case-order-button" title="click and drag to update case order" value="Update case order" />
|
<input type=button id="case-order-button" title="click and drag to update case order" value="Update case order" />
|
||||||
<div id="case_formset" class="sortable">
|
<div id="case_formset" class="sortable list_formset">
|
||||||
|
<ol>
|
||||||
{% for form in case_formset %}
|
{% for form in case_formset %}
|
||||||
<ul class="no-error case-formset">
|
<li class="no-error case-formset">
|
||||||
{{form.non_field_errors}}
|
{{form.non_field_errors}}
|
||||||
{{form.errors}}
|
{{form.errors}}
|
||||||
{{ form.as_ul }}
|
{{ form }}
|
||||||
</ul>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{ case_formset.management_form }}
|
{{ case_formset.management_form }}
|
||||||
<input type="submit" class="submit-button" value="Submit" name="submit">
|
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||||
</form>
|
</form>
|
||||||
<div id="empty_case_form" style="display:none">
|
<div id="empty_case_form" style="display:none">
|
||||||
<ul class='no_error case-formset'>
|
<li class='no_error case-formset'>
|
||||||
{{ case_formset.empty_form.as_ul }}
|
{{ case_formset.empty_form }}
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
|
|||||||
@@ -16,6 +16,20 @@
|
|||||||
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% if not completed %}
|
||||||
|
<h5>Resources</h4>
|
||||||
|
<ul>
|
||||||
|
{% for caseresource in resources %}
|
||||||
|
<li>
|
||||||
|
{{caseresource.resource.get_display}}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% if show_description and case.description %}
|
{% if show_description and case.description %}
|
||||||
<div>
|
<div>
|
||||||
Description: {{case.description}}
|
Description: {{case.description}}
|
||||||
@@ -28,14 +42,18 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if collection.show_ohif_viewer %}
|
||||||
{% if collection.show_ohif_viewer_link %}
|
|
||||||
<div>
|
<div>
|
||||||
<a href="https://viewer.penracourses.org.uk/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">View case in OHIF</a>
|
<iframe id="viewer" style="width: 100%; height: 100%; border: none; padding: 0px;" src="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}"></iframe>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if collection.show_ohif_viewer_link %}
|
||||||
|
<div>
|
||||||
|
<a href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">View case in OHIF</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if collection.show_built_in_viewer %}
|
{% if collection.show_built_in_viewer %}
|
||||||
<div class="pre-whitespace multi-image-block">
|
<div class="pre-whitespace multi-image-block">
|
||||||
@@ -89,8 +107,19 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if collection.publish_results or cid_user_exam.completed %}
|
{% if completed %}
|
||||||
<a href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}"><button>Add self review</button></a>
|
<h5>Resources</h4>
|
||||||
|
<ul>
|
||||||
|
{% for caseresource in resources %}
|
||||||
|
<li>
|
||||||
|
{{caseresource.resource.get_display}}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}"><button>Add self review</button></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST" class="post-form">{% csrf_token %}
|
<form method="POST" class="post-form">{% csrf_token %}
|
||||||
@@ -133,6 +162,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if collection.self_review and not completed %}
|
||||||
|
<button type="submit" name="complete_case" class="save btn btn-default">Finish Case</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<button type="submit" name="finish" class="save btn btn-default">Overview</button>
|
<button type="submit" name="finish" class="save btn btn-default">Overview</button>
|
||||||
<button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button>
|
<button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{% extends 'atlas/exams.html' %}
|
{% extends 'atlas/exams.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>{{collection.name}}</h2>
|
<h2>{{collection.name}}</h2>
|
||||||
|
|
||||||
{% include 'exam_notes.html' %}
|
{% include 'exam_notes.html' %}
|
||||||
<div>
|
<div>
|
||||||
Exam mode: {{collection.exam_mode}}<br />
|
Exam mode: {{collection.exam_mode}}<br />
|
||||||
Publish results: {{collection.publish_results}}<br />
|
Publish results: {{collection.publish_results}}<br />
|
||||||
Active: {{collection.active}}<br />
|
Active: {{collection.active}}<br />
|
||||||
Collection Type: {{collection.collection_type}}<br />
|
Collection Type: {{collection.get_collection_type_display}}<br />
|
||||||
Self review: {{collection.self_review}}<br />
|
Self review: {{collection.self_review}}<br />
|
||||||
</div>
|
</div>
|
||||||
<h3>Cases</h3>
|
<h3>Cases</h3>
|
||||||
@@ -21,18 +21,23 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<p>This collection will be available to view/take <a href='{{collection.get_take_url}}'>here</a>
|
|
||||||
<p>View as a viva collection <a href='{% url "atlas:collection_viva" collection.pk %}'>here</a>
|
|
||||||
|
{% if collection.collection_type == "VIV" %}
|
||||||
|
<p>View as a viva collection <a href='{% url "atlas:collection_viva" collection.pk %}'>here</a>
|
||||||
|
{% else %}
|
||||||
|
<p>This collection will be available to view/take <a href='{{collection.get_take_url}}'>here</a> (when active)
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<p><button id='button-edit-order' title='click and drag questions to change order' data-posturl="{% url 'atlas:exam_json_edit' pk=collection.pk %}">Edit case order / Delete cases</button></p>
|
<p><button id='button-edit-order' title='click and drag questions to change order' data-posturl="{% url 'atlas:exam_json_edit' pk=collection.pk %}">Edit case order / Delete cases</button></p>
|
||||||
<p><button id='button-add-case'
|
<p><button id='button-add-case'
|
||||||
title='click to add a case to this collection'
|
title='click to add a case to this collection'
|
||||||
hx-get="{% url 'atlas:add_case_to_collection' collection.pk %}" hx-swap="innerHTML" hx-target="#case-list">
|
hx-get="{% url 'atlas:add_case_to_collection' collection.pk %}" hx-swap="innerHTML" hx-target="#case-list">
|
||||||
Add case
|
Add case
|
||||||
</button>
|
</button>
|
||||||
<div id='case-list'></div>
|
<div id='case-list'></div>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'exam_overview_js.html' %}
|
{% include 'exam_overview_js.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -27,14 +27,23 @@
|
|||||||
{% url 'atlas:collection_case_view_take_user' pk=collection.id case_number=forloop.counter0 %}
|
{% url 'atlas:collection_case_view_take_user' pk=collection.id case_number=forloop.counter0 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
">
|
">
|
||||||
Case: {{forloop.counter}}{% if self_review %} <span class="self-review-complete" title="You have reviewed this case."> <i class="bi-card-checklist"></i></span>{% endif %}
|
Case: {{forloop.counter}}
|
||||||
|
|
||||||
|
{% if answer.completed or cid_user_exam.completed %}
|
||||||
|
<span title="You have completed this case"><i class="bi bi-check-square-fill"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if self_review %} <span class="self-review-complete" title="You have reviewed this case."> <i class="bi-card-checklist"></i></span>{% endif %}
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
{% if not collection.review_only %}
|
{% if not collection.review_only %}
|
||||||
{% if not answer %}
|
{% if not answer %}
|
||||||
No answer
|
No answer
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<details><summary>Answered</summary>
|
||||||
{{answer.answer}}
|
{{answer.answer}}
|
||||||
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
{% extends "atlas/exams.html" %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ form.media }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<h2>Update Exam</h2>
|
|
||||||
<form action="" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<table>
|
|
||||||
{{ form.as_table }}
|
|
||||||
</table>
|
|
||||||
<input type="submit" value="Submit">
|
|
||||||
</form>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -3,7 +3,11 @@
|
|||||||
{% block navigation %}
|
{% block navigation %}
|
||||||
{{block.super}}
|
{{block.super}}
|
||||||
|
|
||||||
{% include "atlas/collection_headers.html" %}
|
|
||||||
|
{% if collection %}
|
||||||
|
{% include "atlas/collection_headers.html" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% comment %} Collection: {{collection.name}}-> <a href="{% url 'atlas:collection_detail' pk=exam.pk %}">Overview</a> /
|
{% comment %} Collection: {{collection.name}}-> <a href="{% url 'atlas:collection_detail' pk=exam.pk %}">Overview</a> /
|
||||||
<a href="{% url 'atlas:collection_mark_overview' exam.pk %}">Mark</a> /
|
<a href="{% url 'atlas:collection_mark_overview' exam.pk %}">Mark</a> /
|
||||||
<a href="{% url 'atlas:collection_scores_cid' exam.pk %}">Scores</a> /
|
<a href="{% url 'atlas:collection_scores_cid' exam.pk %}">Scores</a> /
|
||||||
|
|||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends 'atlas/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if resource %}
|
||||||
|
{% include "atlas/resource_link_header.html" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{{resource}}
|
||||||
|
|
||||||
|
Name: {{resource.name}}<br>
|
||||||
|
Description: {{resource.description}}<br>
|
||||||
|
URL: {{resource.url}}<br>
|
||||||
|
file: {{resource.file}}<br>
|
||||||
|
author: {{resource.get_authors}}<br>
|
||||||
|
|
||||||
|
{{resource.case}}
|
||||||
|
|
||||||
|
{% if resource.case_set.all %}
|
||||||
|
<h3>Cases</h3>
|
||||||
|
This resource is used in the following cases:
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for case in resource.case_set.all %}
|
||||||
|
<li><a href="{% url 'atlas:case_detail' case.id %}">{{case.title}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>This resource is not used in any cases.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Executable
+26
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends "atlas/base.html" %}
|
||||||
|
<!-- {% load static from static %} -->
|
||||||
|
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block js %}
|
||||||
|
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
||||||
|
{{form.media}}
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- {{ form.media }} -->
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
<h2>Edit Resource {{object.name}}</h2>
|
||||||
|
<form action="" method="post" enctype="multipart/form-data" id="resource-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="floating-header">
|
||||||
|
|
||||||
|
|
||||||
|
<a href="{% url 'atlas:resource_update' pk=resource.pk %}" title="Edit the resource">Edit</a>
|
||||||
|
<a href="{% url 'atlas:resource_delete' pk=resource.pk %}" title="Delete the resource">Delete</a>
|
||||||
|
{% if request.user.is_superuser %}
|
||||||
|
<a href="{% url 'admin:atlas_resource_change' resource.id %}" title="Edit the resource using the admin interface">Admin Edit</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends 'atlas/base.html' %}
|
||||||
|
{% block content %}
|
||||||
|
<a href="{% url 'atlas:resource_create' %}">Create a new resource</a>
|
||||||
|
|
||||||
|
<h2>Resources</h2>
|
||||||
|
|
||||||
|
<ul id="resource-list">
|
||||||
|
{% for resource in object_list %}
|
||||||
|
<li class="">
|
||||||
|
<a href="{% url 'atlas:resource_detail' resource.pk %}">{{resource}}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<style>
|
||||||
|
td, th { padding-left: 10px }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form.non_field_errors }}
|
{{ form.non_field_errors }}
|
||||||
|
|
||||||
{{ form.as_p }}
|
{{ form }}
|
||||||
{% comment %} <div class="fieldWrapper">
|
{% comment %} <div class="fieldWrapper">
|
||||||
{{ form.content_type.errors }}
|
{{ form.content_type.errors }}
|
||||||
{{ form.content_type }}
|
{{ form.content_type }}
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ urlpatterns = [
|
|||||||
path("uploads/case/<int:case_id>", views.user_uploads, name="user_uploads_case"),
|
path("uploads/case/<int:case_id>", views.user_uploads, name="user_uploads_case"),
|
||||||
path("uploads/<int:user_pk>/user/case/<int:case_id>", views.other_user_uploads, name="other_user_uploads_case"),
|
path("uploads/<int:user_pk>/user/case/<int:case_id>", views.other_user_uploads, name="other_user_uploads_case"),
|
||||||
path("uploads/series_id/<str:series_instance_uid>", views.user_uploads_series, name="user_uploads_series"),
|
path("uploads/series_id/<str:series_instance_uid>", views.user_uploads_series, name="user_uploads_series"),
|
||||||
|
path("resource/", views.ResourceView.as_view(), name="resource_view"),
|
||||||
|
path("resource/<int:pk>/", views.resource_detail, name="resource_detail"),
|
||||||
|
path("resource/<int:pk>/update", views.ResourceUpdate.as_view(), name="resource_update"),
|
||||||
|
path("resource/<int:pk>/delete", views.ResourceDelete.as_view(), name="resource_delete"),
|
||||||
|
path(
|
||||||
|
"resource/create",
|
||||||
|
views.ResourceCreate.as_view(),
|
||||||
|
name="resource_create",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"collection/create",
|
"collection/create",
|
||||||
views.CaseCollectionCreate.as_view(),
|
views.CaseCollectionCreate.as_view(),
|
||||||
@@ -74,6 +83,21 @@ urlpatterns = [
|
|||||||
views.GenericExamViews.exam_cids,
|
views.GenericExamViews.exam_cids,
|
||||||
name="exam_cids",
|
name="exam_cids",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"collection/<int:pk>/user_status",
|
||||||
|
views.GenericExamViews.exam_user_status,
|
||||||
|
name="exam_user_status",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"collection/<int:pk>/user_status/<int:cid>/cid",
|
||||||
|
views.GenericExamViews.exam_user_status_cid,
|
||||||
|
name="exam_user_status_cid",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"collection/<int:pk>/user_status/<int:user_id>/user",
|
||||||
|
views.GenericExamViews.exam_user_status_user,
|
||||||
|
name="exam_user_status_user",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"exam/<int:exam_id>/cids/edit",
|
"exam/<int:exam_id>/cids/edit",
|
||||||
views.GenericExamViews.exam_cids_edit,
|
views.GenericExamViews.exam_cids_edit,
|
||||||
|
|||||||
+162
-48
@@ -31,6 +31,7 @@ from django.http import Http404, JsonResponse
|
|||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
|
|
||||||
from generic.models import CidUser, CidUserExam
|
from generic.models import CidUser, CidUserExam
|
||||||
|
from atlas.helpers import get_cases_available_to_user
|
||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
AddCollectionToCaseForm,
|
AddCollectionToCaseForm,
|
||||||
@@ -45,6 +46,7 @@ from .forms import (
|
|||||||
ConditionAutocompleteForm,
|
ConditionAutocompleteForm,
|
||||||
ConditionForm,
|
ConditionForm,
|
||||||
FindingForm,
|
FindingForm,
|
||||||
|
ResourceForm,
|
||||||
SelfReviewForm,
|
SelfReviewForm,
|
||||||
SeriesForm,
|
SeriesForm,
|
||||||
SeriesImageFormSet,
|
SeriesImageFormSet,
|
||||||
@@ -63,6 +65,7 @@ from .models import (
|
|||||||
Differential,
|
Differential,
|
||||||
PathologicalProcess,
|
PathologicalProcess,
|
||||||
Presentation,
|
Presentation,
|
||||||
|
Resource,
|
||||||
SelfReview,
|
SelfReview,
|
||||||
Series,
|
Series,
|
||||||
Examination,
|
Examination,
|
||||||
@@ -127,7 +130,12 @@ import logging
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from django.forms.models import model_to_dict
|
from django.forms.models import model_to_dict
|
||||||
|
|
||||||
from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamViews, SeriesImagesZipViewBase
|
from generic.views import (
|
||||||
|
AuthorRequiredMixin,
|
||||||
|
ExamCloneMixin,
|
||||||
|
ExamViews,
|
||||||
|
SeriesImagesZipViewBase,
|
||||||
|
)
|
||||||
from reversion.views import RevisionMixin
|
from reversion.views import RevisionMixin
|
||||||
import reversion
|
import reversion
|
||||||
|
|
||||||
@@ -139,14 +147,6 @@ import difflib
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def get_cases_available_to_user(user):
|
|
||||||
"""Returns a queryset of cases available to the user"""
|
|
||||||
return Case.objects.filter(author=user) | Case.objects.filter(open_access=True)
|
|
||||||
if user.is_superuser:
|
|
||||||
return Case.objects.all()
|
|
||||||
else:
|
|
||||||
return Case.objects.filter(author=user) | Case.objects.filter(open_access=True)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorOrCheckerRequiredMixin(object):
|
class AuthorOrCheckerRequiredMixin(object):
|
||||||
def get_object(self, *args, **kwargs):
|
def get_object(self, *args, **kwargs):
|
||||||
@@ -160,6 +160,7 @@ class AuthorOrCheckerRequiredMixin(object):
|
|||||||
raise PermissionDenied() # or Http404
|
raise PermissionDenied() # or Http404
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class AtlasEditorRequiredMixin(object):
|
class AtlasEditorRequiredMixin(object):
|
||||||
def get_object(self, *args, **kwargs):
|
def get_object(self, *args, **kwargs):
|
||||||
obj = super().get_object(*args, **kwargs)
|
obj = super().get_object(*args, **kwargs)
|
||||||
@@ -170,6 +171,7 @@ class AtlasEditorRequiredMixin(object):
|
|||||||
return obj
|
return obj
|
||||||
raise PermissionDenied("You must be an atlas editor to do this.") # or Http404
|
raise PermissionDenied("You must be an atlas editor to do this.") # or Http404
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_has_case_view_access
|
@user_has_case_view_access
|
||||||
def case_detail(request, pk):
|
def case_detail(request, pk):
|
||||||
@@ -208,7 +210,7 @@ def series_detail(request, pk, finding_pk=None):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
#@user_is_atlas_editor
|
# @user_is_atlas_editor
|
||||||
def condition_detail(request, pk):
|
def condition_detail(request, pk):
|
||||||
condition = get_object_or_404(Condition, pk=pk)
|
condition = get_object_or_404(Condition, pk=pk)
|
||||||
|
|
||||||
@@ -216,7 +218,10 @@ def condition_detail(request, pk):
|
|||||||
|
|
||||||
can_merge = False
|
can_merge = False
|
||||||
|
|
||||||
if request.user.is_superuser or request.user.groups.filter(name="atlas_editor").exists():
|
if (
|
||||||
|
request.user.is_superuser
|
||||||
|
or request.user.groups.filter(name="atlas_editor").exists()
|
||||||
|
):
|
||||||
can_merge = True
|
can_merge = True
|
||||||
|
|
||||||
# logging.debug(atlas.subspecialty.first().name.all())
|
# logging.debug(atlas.subspecialty.first().name.all())
|
||||||
@@ -275,7 +280,7 @@ def condition_merge(request, pk_to_merge):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
#@user_is_atlas_editor
|
# @user_is_atlas_editor
|
||||||
def subspecialty_detail(request, pk):
|
def subspecialty_detail(request, pk):
|
||||||
subspecialty = get_object_or_404(Subspecialty, pk=pk)
|
subspecialty = get_object_or_404(Subspecialty, pk=pk)
|
||||||
|
|
||||||
@@ -290,7 +295,7 @@ def subspecialty_detail(request, pk):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
#@user_is_atlas_editor
|
# @user_is_atlas_editor
|
||||||
def presentation_detail(request, pk):
|
def presentation_detail(request, pk):
|
||||||
presentation = get_object_or_404(Presentation, pk=pk)
|
presentation = get_object_or_404(Presentation, pk=pk)
|
||||||
|
|
||||||
@@ -305,7 +310,7 @@ def presentation_detail(request, pk):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
#@user_is_atlas_editor
|
# @user_is_atlas_editor
|
||||||
def pathological_process_detail(request, pk):
|
def pathological_process_detail(request, pk):
|
||||||
pathological_process = get_object_or_404(PathologicalProcess, pk=pk)
|
pathological_process = get_object_or_404(PathologicalProcess, pk=pk)
|
||||||
|
|
||||||
@@ -320,7 +325,7 @@ def pathological_process_detail(request, pk):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
#@user_is_atlas_editor
|
# @user_is_atlas_editor
|
||||||
def structure_detail(request, pk):
|
def structure_detail(request, pk):
|
||||||
structure = get_object_or_404(Structure, pk=pk)
|
structure = get_object_or_404(Structure, pk=pk)
|
||||||
|
|
||||||
@@ -335,7 +340,7 @@ def structure_detail(request, pk):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
#@user_is_atlas_editor
|
# @user_is_atlas_editor
|
||||||
def finding_detail(request, pk):
|
def finding_detail(request, pk):
|
||||||
finding = get_object_or_404(Finding, pk=pk)
|
finding = get_object_or_404(Finding, pk=pk)
|
||||||
|
|
||||||
@@ -349,6 +354,13 @@ def finding_detail(request, pk):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def resource_detail(request, pk):
|
||||||
|
resource = get_object_or_404(Resource, pk=pk)
|
||||||
|
|
||||||
|
return render(request, "atlas/resource_detail.html", {"resource": resource})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def author_detail(request, pk):
|
def author_detail(request, pk):
|
||||||
# logging.debug(Author.objects.all())
|
# logging.debug(Author.objects.all())
|
||||||
@@ -381,11 +393,11 @@ def user_collections(request):
|
|||||||
|
|
||||||
return render(request, "atlas/user_collections.html", {"collections": collections})
|
return render(request, "atlas/user_collections.html", {"collections": collections})
|
||||||
|
|
||||||
|
|
||||||
def add_case_to_collection(request, collection_id):
|
def add_case_to_collection(request, collection_id):
|
||||||
if not request.htmx:
|
if not request.htmx:
|
||||||
return Http404
|
return Http404
|
||||||
|
|
||||||
|
|
||||||
collection = get_object_or_404(CaseCollection, pk=collection_id)
|
collection = get_object_or_404(CaseCollection, pk=collection_id)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -395,14 +407,18 @@ def add_case_to_collection(request, collection_id):
|
|||||||
return HttpResponse("Case already in collection")
|
return HttpResponse("Case already in collection")
|
||||||
print(case)
|
print(case)
|
||||||
collection.add_case(case)
|
collection.add_case(case)
|
||||||
return HttpResponse("Case added to collection")
|
return HttpResponse("Case added to collection (refresh to see)")
|
||||||
|
|
||||||
|
# if request.method == "POST":
|
||||||
#if request.method == "POST":
|
|
||||||
|
|
||||||
cases = get_cases_available_to_user(request.user)
|
cases = get_cases_available_to_user(request.user)
|
||||||
|
|
||||||
return render(request, "atlas/add_case_to_collection.html", {"cases": cases, "collection": collection})
|
return render(
|
||||||
|
request,
|
||||||
|
"atlas/add_case_to_collection.html",
|
||||||
|
{"cases": cases, "collection": collection},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def user_uploads_series(request, series_instance_uid: str):
|
def user_uploads_series(request, series_instance_uid: str):
|
||||||
@@ -418,17 +434,25 @@ def user_uploads_series(request, series_instance_uid: str):
|
|||||||
{"dicoms": dicoms, "series_id": series_instance_uid},
|
{"dicoms": dicoms, "series_id": series_instance_uid},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_passes_test(lambda u: u.is_superuser)
|
@user_passes_test(lambda u: u.is_superuser)
|
||||||
def all_uploads(request, case_id: int | None = None):
|
def all_uploads(request, case_id: int | None = None):
|
||||||
return user_uploads(request, case_id, user_only=False)
|
return user_uploads(request, case_id, user_only=False)
|
||||||
|
|
||||||
|
|
||||||
@user_passes_test(lambda u: u.is_superuser)
|
@user_passes_test(lambda u: u.is_superuser)
|
||||||
def other_user_uploads(request, user_pk: int, case_id: int | None = None):
|
def other_user_uploads(request, user_pk: int, case_id: int | None = None):
|
||||||
return user_uploads(request, case_id, user_only=False, user_pk=user_pk)
|
return user_uploads(request, case_id, user_only=False, user_pk=user_pk)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def user_uploads(request, case_id: int | None = None, user_only: bool = True, user_pk: int | None = None):
|
def user_uploads(
|
||||||
|
request,
|
||||||
|
case_id: int | None = None,
|
||||||
|
user_only: bool = True,
|
||||||
|
user_pk: int | None = None,
|
||||||
|
):
|
||||||
user = request.user
|
user = request.user
|
||||||
if user_only:
|
if user_only:
|
||||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||||
@@ -446,7 +470,6 @@ def user_uploads(request, case_id: int | None = None, user_only: bool = True, us
|
|||||||
d.save()
|
d.save()
|
||||||
tags = d.basic_dicom_tags
|
tags = d.basic_dicom_tags
|
||||||
|
|
||||||
|
|
||||||
data[tags["SeriesInstanceUID"]].append((tags, d.created_date))
|
data[tags["SeriesInstanceUID"]].append((tags, d.created_date))
|
||||||
|
|
||||||
series_list = []
|
series_list = []
|
||||||
@@ -461,7 +484,9 @@ def user_uploads(request, case_id: int | None = None, user_only: bool = True, us
|
|||||||
case = get_object_or_404(Case, pk=case_id)
|
case = get_object_or_404(Case, pk=case_id)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request, "atlas/user_uploads.html", {"series_list": series_list, "case": case, "user": user}
|
request,
|
||||||
|
"atlas/user_uploads.html",
|
||||||
|
{"series_list": series_list, "case": case, "user": user},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -513,9 +538,34 @@ class StructureDelete(RevisionMixin, AtlasEditorRequiredMixin, DeleteView):
|
|||||||
success_url = reverse_lazy("atlas:structure_view")
|
success_url = reverse_lazy("atlas:structure_view")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceDelete(LoginRequiredMixin, DeleteView):
|
||||||
|
model = Resource
|
||||||
|
template_name = "confirm_delete.html"
|
||||||
|
|
||||||
|
success_url = reverse_lazy("atlas:resource_view")
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceCreate(LoginRequiredMixin, CreateView):
|
||||||
|
model = Resource
|
||||||
|
form_class = ResourceForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.object = form.save(commit=False)
|
||||||
|
self.object.save()
|
||||||
|
form.instance.author.add(self.request.user.id)
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceView(LoginRequiredMixin, ListView):
|
||||||
|
model = Resource
|
||||||
|
# form_class = ResourceForm
|
||||||
|
|
||||||
|
|
||||||
class CaseCollectionCreate(RevisionMixin, LoginRequiredMixin, CreateView):
|
class CaseCollectionCreate(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||||
model = CaseCollection
|
model = CaseCollection
|
||||||
form_class = CaseCollectionForm
|
form_class = CaseCollectionForm
|
||||||
|
template_name = "atlas/casecollection_form.html"
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
# print(self.request)
|
# print(self.request)
|
||||||
@@ -553,7 +603,6 @@ class CaseCollectionCreate(RevisionMixin, LoginRequiredMixin, CreateView):
|
|||||||
self.object.save()
|
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)
|
context = self.get_context_data(form=form)
|
||||||
case_formset = context["case_formset"]
|
case_formset = context["case_formset"]
|
||||||
if case_formset.is_valid():
|
if case_formset.is_valid():
|
||||||
@@ -618,7 +667,7 @@ class SeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView):
|
|||||||
class CaseCollectionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
class CaseCollectionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
||||||
model = CaseCollection
|
model = CaseCollection
|
||||||
form_class = CaseCollectionForm
|
form_class = CaseCollectionForm
|
||||||
template_name = "atlas/collection_update_form.html"
|
template_name = "atlas/casecollection_form.html"
|
||||||
|
|
||||||
# fields = '__all__'
|
# fields = '__all__'
|
||||||
# #fields = [ 'condition' ]
|
# #fields = [ 'condition' ]
|
||||||
@@ -724,6 +773,18 @@ class SeriesUpdate(
|
|||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceUpdate(LoginRequiredMixin, UpdateView):
|
||||||
|
model = Resource
|
||||||
|
form_class = ResourceForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.object = form.save(commit=False)
|
||||||
|
self.object.save()
|
||||||
|
form.instance.author.add(self.request.user.id)
|
||||||
|
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class ConditionUpdate(RevisionMixin, LoginRequiredMixin, UpdateView):
|
class ConditionUpdate(RevisionMixin, LoginRequiredMixin, UpdateView):
|
||||||
model = Condition
|
model = Condition
|
||||||
form_class = ConditionForm
|
form_class = ConditionForm
|
||||||
@@ -775,7 +836,8 @@ class AtlasCreateBase(RevisionMixin, LoginRequiredMixin):
|
|||||||
self.request.POST, self.request.FILES
|
self.request.POST, self.request.FILES
|
||||||
)
|
)
|
||||||
context["caseresource_formset"] = CaseResourceFormSet(
|
context["caseresource_formset"] = CaseResourceFormSet(
|
||||||
self.request.POST, self.request.FILES,
|
self.request.POST,
|
||||||
|
self.request.FILES,
|
||||||
form_kwargs={"user": self.request.user},
|
form_kwargs={"user": self.request.user},
|
||||||
)
|
)
|
||||||
context["series_formset"].full_clean()
|
context["series_formset"].full_clean()
|
||||||
@@ -805,7 +867,11 @@ class AtlasCreateBase(RevisionMixin, LoginRequiredMixin):
|
|||||||
series_formset = context["series_formset"]
|
series_formset = context["series_formset"]
|
||||||
casedifferential_formset = context["casedifferential_formset"]
|
casedifferential_formset = context["casedifferential_formset"]
|
||||||
caseresource_formset = context["caseresource_formset"]
|
caseresource_formset = context["caseresource_formset"]
|
||||||
if series_formset.is_valid() and casedifferential_formset.is_valid() and caseresource_formset.is_valid():
|
if (
|
||||||
|
series_formset.is_valid()
|
||||||
|
and casedifferential_formset.is_valid()
|
||||||
|
and caseresource_formset.is_valid()
|
||||||
|
):
|
||||||
response = super().form_valid(form)
|
response = super().form_valid(form)
|
||||||
series_formset.instance = self.object
|
series_formset.instance = self.object
|
||||||
series_formset.save()
|
series_formset.save()
|
||||||
@@ -871,8 +937,15 @@ class AtlasUpdate(
|
|||||||
context["casedifferential_formset"] = CaseDifferentialFormSet(
|
context["casedifferential_formset"] = CaseDifferentialFormSet(
|
||||||
self.request.POST, self.request.FILES, instance=self.object
|
self.request.POST, self.request.FILES, instance=self.object
|
||||||
)
|
)
|
||||||
|
context["caseresource_formset"] = CaseResourceFormSet(
|
||||||
|
self.request.POST,
|
||||||
|
self.request.FILES,
|
||||||
|
instance=self.object,
|
||||||
|
form_kwargs={"user": self.request.user},
|
||||||
|
)
|
||||||
context["series_formset"].full_clean()
|
context["series_formset"].full_clean()
|
||||||
context["casedifferential_formset"].full_clean()
|
context["casedifferential_formset"].full_clean()
|
||||||
|
context["caseresource_formset"].full_clean()
|
||||||
else:
|
else:
|
||||||
context["series_formset"] = SeriesFormSet(
|
context["series_formset"] = SeriesFormSet(
|
||||||
instance=self.object, form_kwargs={"user": self.request.user}
|
instance=self.object, form_kwargs={"user": self.request.user}
|
||||||
@@ -880,6 +953,10 @@ class AtlasUpdate(
|
|||||||
context["casedifferential_formset"] = CaseDifferentialFormSet(
|
context["casedifferential_formset"] = CaseDifferentialFormSet(
|
||||||
instance=self.object
|
instance=self.object
|
||||||
)
|
)
|
||||||
|
context["caseresource_formset"] = CaseResourceFormSet(
|
||||||
|
instance=self.object,
|
||||||
|
form_kwargs={"user": self.request.user},
|
||||||
|
)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
@@ -891,13 +968,20 @@ class AtlasUpdate(
|
|||||||
context = self.get_context_data(form=form)
|
context = self.get_context_data(form=form)
|
||||||
series_formset = context["series_formset"]
|
series_formset = context["series_formset"]
|
||||||
casedifferential_formset = context["casedifferential_formset"]
|
casedifferential_formset = context["casedifferential_formset"]
|
||||||
|
caseresource_formset = context["caseresource_formset"]
|
||||||
# logger.debug(formset.is_valid())
|
# logger.debug(formset.is_valid())
|
||||||
if series_formset.is_valid() and casedifferential_formset.is_valid():
|
if (
|
||||||
|
series_formset.is_valid()
|
||||||
|
and casedifferential_formset.is_valid()
|
||||||
|
and caseresource_formset.is_valid()
|
||||||
|
):
|
||||||
response = super().form_valid(form)
|
response = super().form_valid(form)
|
||||||
series_formset.instance = self.object
|
series_formset.instance = self.object
|
||||||
series_formset.save()
|
series_formset.save()
|
||||||
casedifferential_formset.instance = self.object
|
casedifferential_formset.instance = self.object
|
||||||
casedifferential_formset.save()
|
casedifferential_formset.save()
|
||||||
|
caseresource_formset.instance = self.object
|
||||||
|
caseresource_formset.save()
|
||||||
|
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
@@ -970,7 +1054,6 @@ class AtlasClone(AtlasCreateBase, AuthorOrCheckerRequiredMixin, CreateView):
|
|||||||
return initial_data
|
return initial_data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_is_author_or_atlas_editor
|
@user_is_author_or_atlas_editor
|
||||||
def atlas_scrap(request, pk):
|
def atlas_scrap(request, pk):
|
||||||
@@ -1069,6 +1152,7 @@ class FindingView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
|||||||
|
|
||||||
filterset_class = FindingFilter
|
filterset_class = FindingFilter
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def case_order_dicom(request, pk):
|
def case_order_dicom(request, pk):
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
@@ -1086,11 +1170,12 @@ def case_order_dicom(request, pk):
|
|||||||
fail.append(series.pk)
|
fail.append(series.pk)
|
||||||
|
|
||||||
if fail:
|
if fail:
|
||||||
return HttpResponse(f"Unable to order series {','.join([str(i) for i in fail])}")
|
return HttpResponse(
|
||||||
|
f"Unable to order series {','.join([str(i) for i in fail])}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return HttpResponse("Series reordered")
|
return HttpResponse("Series reordered")
|
||||||
|
|
||||||
|
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
|
||||||
@@ -1105,6 +1190,7 @@ def series_order_dicom(request, pk):
|
|||||||
|
|
||||||
return redirect("atlas:series_detail", pk=pk)
|
return redirect("atlas:series_detail", pk=pk)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def image_diff(request):
|
def image_diff(request):
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
@@ -1116,23 +1202,23 @@ def image_diff(request):
|
|||||||
second: SeriesImage = get_object_or_404(SeriesImage, pk=image_ids[1])
|
second: SeriesImage = get_object_or_404(SeriesImage, pk=image_ids[1])
|
||||||
|
|
||||||
first_ds = pydicom.read_file(first.image.file)
|
first_ds = pydicom.read_file(first.image.file)
|
||||||
#del first_ds.PixelData
|
# del first_ds.PixelData
|
||||||
#j1 = first_ds.to_json_dict()
|
# j1 = first_ds.to_json_dict()
|
||||||
f = print_dicom(first_ds, join=False)
|
f = print_dicom(first_ds, join=False)
|
||||||
|
|
||||||
second_ds = pydicom.read_file(second.image.file)
|
second_ds = pydicom.read_file(second.image.file)
|
||||||
#del second_ds.PixelData
|
# del second_ds.PixelData
|
||||||
#j2 = second_ds.to_json_dict()
|
# j2 = second_ds.to_json_dict()
|
||||||
s = print_dicom(second_ds, join=False)
|
s = print_dicom(second_ds, join=False)
|
||||||
|
|
||||||
diff = difflib.HtmlDiff().make_table(f, s)
|
diff = difflib.HtmlDiff().make_table(f, s)
|
||||||
|
|
||||||
dict_diff = pprint.pformat(compare_dicom_datasets(first_ds, second_ds))
|
dict_diff = pprint.pformat(compare_dicom_datasets(first_ds, second_ds))
|
||||||
|
|
||||||
#return HttpResponse(diff)
|
# return HttpResponse(diff)
|
||||||
|
|
||||||
return HttpResponse(f"<pre>{dict_diff}</pre><br/>{diff}")
|
return HttpResponse(f"<pre>{dict_diff}</pre><br/>{diff}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ds_set = []
|
ds_set = []
|
||||||
for pk in image_ids:
|
for pk in image_ids:
|
||||||
@@ -1142,7 +1228,6 @@ def image_diff(request):
|
|||||||
dict_diff = pprint.pformat(compare_dicom_datasets(*ds_set))
|
dict_diff = pprint.pformat(compare_dicom_datasets(*ds_set))
|
||||||
return HttpResponse(f"<pre>{dict_diff}</pre>")
|
return HttpResponse(f"<pre>{dict_diff}</pre>")
|
||||||
|
|
||||||
|
|
||||||
return HttpResponse("Fail")
|
return HttpResponse("Fail")
|
||||||
|
|
||||||
|
|
||||||
@@ -1366,6 +1451,7 @@ class CollectionView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
|||||||
|
|
||||||
filterset_class = CaseCollectionFilter
|
filterset_class = CaseCollectionFilter
|
||||||
|
|
||||||
|
|
||||||
@user_is_collection_author_or_atlas_editor
|
@user_is_collection_author_or_atlas_editor
|
||||||
def collection_viva(request, pk):
|
def collection_viva(request, pk):
|
||||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||||
@@ -1378,11 +1464,12 @@ def collection_viva(request, pk):
|
|||||||
{"collection": collection, "cases": cases, "can_edit": True},
|
{"collection": collection, "cases": cases, "can_edit": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_is_collection_author_or_atlas_editor
|
@user_is_collection_author_or_atlas_editor
|
||||||
def collection_viva_case(request, pk, case_number):
|
def collection_viva_case(request, pk, case_number):
|
||||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||||
|
|
||||||
#cases = collection.cases.all().order_by("casedetail__sort_order")
|
# cases = collection.cases.all().order_by("casedetail__sort_order")
|
||||||
current_case = collection.get_case_by_index(case_number)
|
current_case = collection.get_case_by_index(case_number)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
@@ -1391,6 +1478,7 @@ def collection_viva_case(request, pk, case_number):
|
|||||||
{"collection": collection, "current_case": current_case},
|
{"collection": collection, "current_case": current_case},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_is_collection_author_or_atlas_editor
|
@user_is_collection_author_or_atlas_editor
|
||||||
def collection_viva_series(request, pk, series_id):
|
def collection_viva_series(request, pk, series_id):
|
||||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||||
@@ -1408,8 +1496,10 @@ def collection_viva_series(request, pk, series_id):
|
|||||||
def collection_detail(request, pk):
|
def collection_detail(request, pk):
|
||||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||||
|
|
||||||
#cases = collection.cases.all().order_by("casedetail__sort_order")
|
# cases = collection.cases.all().order_by("casedetail__sort_order")
|
||||||
casedetails = CaseDetail.objects.filter(collection=collection).order_by("sort_order")
|
casedetails = CaseDetail.objects.filter(collection=collection).order_by(
|
||||||
|
"sort_order"
|
||||||
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -1725,12 +1815,20 @@ def collection_case_view_take(
|
|||||||
else:
|
else:
|
||||||
form = ReportAnswerForm(request.POST)
|
form = ReportAnswerForm(request.POST)
|
||||||
|
|
||||||
if not cid_user_exam.completed:
|
if not cid_user_exam.completed and not (
|
||||||
|
answer is not None and answer.completed
|
||||||
|
):
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
answer = form.save(commit=False)
|
answer = form.save(commit=False)
|
||||||
answer.set_cid_or_user(cid=cid, user=request.user)
|
answer.set_cid_or_user(cid=cid, user=request.user)
|
||||||
answer.question = case_detail
|
answer.question = case_detail
|
||||||
# answer.published_date = timezone.now()
|
# answer.published_date = timezone.now()
|
||||||
|
|
||||||
|
if "complete_case" in request.POST:
|
||||||
|
if not collection.self_review:
|
||||||
|
raise Http404("Self review not enabled")
|
||||||
|
answer.completed = True
|
||||||
|
|
||||||
answer.save()
|
answer.save()
|
||||||
|
|
||||||
cid_user_exam.end_time = timezone.now()
|
cid_user_exam.end_time = timezone.now()
|
||||||
@@ -1769,7 +1867,11 @@ def collection_case_view_take(
|
|||||||
|
|
||||||
# Set what details a user is / is not able to view when taking the case
|
# Set what details a user is / is not able to view when taking the case
|
||||||
# this can be configured via the collection edit menu
|
# this can be configured via the collection edit menu
|
||||||
if collection.publish_results or cid_user_exam.completed:
|
if (
|
||||||
|
collection.publish_results
|
||||||
|
or cid_user_exam.completed
|
||||||
|
or (answer is not None and answer.completed)
|
||||||
|
):
|
||||||
completed = True
|
completed = True
|
||||||
show_title = collection.show_title_post
|
show_title = collection.show_title_post
|
||||||
show_history = collection.show_history_post
|
show_history = collection.show_history_post
|
||||||
@@ -1778,6 +1880,8 @@ def collection_case_view_take(
|
|||||||
show_report = collection.show_report_post
|
show_report = collection.show_report_post
|
||||||
|
|
||||||
self_review = cid_user_exam.selfreview_set.filter(case=case)
|
self_review = cid_user_exam.selfreview_set.filter(case=case)
|
||||||
|
|
||||||
|
resources = case.caseresource_set.all()
|
||||||
else:
|
else:
|
||||||
completed = False
|
completed = False
|
||||||
show_title = collection.show_title_pre
|
show_title = collection.show_title_pre
|
||||||
@@ -1787,6 +1891,8 @@ def collection_case_view_take(
|
|||||||
show_report = collection.show_report_pre
|
show_report = collection.show_report_pre
|
||||||
self_review = []
|
self_review = []
|
||||||
|
|
||||||
|
resources = case.caseresource_set.filter(pre_review=True)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"atlas/collection_case_view_take.html",
|
"atlas/collection_case_view_take.html",
|
||||||
@@ -1806,6 +1912,7 @@ def collection_case_view_take(
|
|||||||
"show_description": show_description,
|
"show_description": show_description,
|
||||||
"show_discussion": show_discussion,
|
"show_discussion": show_discussion,
|
||||||
"show_report": show_report,
|
"show_report": show_report,
|
||||||
|
"resources": resources,
|
||||||
"cid_user_exam": cid_user_exam,
|
"cid_user_exam": cid_user_exam,
|
||||||
"completed": completed,
|
"completed": completed,
|
||||||
"self_review": self_review,
|
"self_review": self_review,
|
||||||
@@ -1856,11 +1963,13 @@ def collection_case_view(request, pk, case_number):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def collection_dicom_json(request, pk):
|
def collection_dicom_json(request, pk):
|
||||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||||
|
|
||||||
return JsonResponse(collection.get_ohif_dicom_json())
|
return JsonResponse(collection.get_ohif_dicom_json())
|
||||||
|
|
||||||
|
|
||||||
@user_is_author_or_atlas_editor
|
@user_is_author_or_atlas_editor
|
||||||
def use_dates_as_descriptions(request, pk):
|
def use_dates_as_descriptions(request, pk):
|
||||||
case = get_object_or_404(Case, pk=pk)
|
case = get_object_or_404(Case, pk=pk)
|
||||||
@@ -1871,16 +1980,19 @@ def use_dates_as_descriptions(request, pk):
|
|||||||
|
|
||||||
return HttpResponse("Done")
|
return HttpResponse("Done")
|
||||||
|
|
||||||
|
|
||||||
def case_dicom_json(request, pk):
|
def case_dicom_json(request, pk):
|
||||||
case = get_object_or_404(Case, pk=pk)
|
case = get_object_or_404(Case, pk=pk)
|
||||||
|
|
||||||
return JsonResponse(case.get_case_dicom_json())
|
return JsonResponse(case.get_case_dicom_json())
|
||||||
|
|
||||||
|
|
||||||
def series_dicom_json(request, pk):
|
def series_dicom_json(request, pk):
|
||||||
series = get_object_or_404(Series, pk=pk)
|
series = get_object_or_404(Series, pk=pk)
|
||||||
|
|
||||||
return JsonResponse(series.get_ohif_dicom_json())
|
return JsonResponse(series.get_ohif_dicom_json())
|
||||||
|
|
||||||
|
|
||||||
@user_is_collection_author_or_atlas_editor
|
@user_is_collection_author_or_atlas_editor
|
||||||
def delete_collection_cid_answers(request, exam_id, cid):
|
def delete_collection_cid_answers(request, exam_id, cid):
|
||||||
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
collection = get_object_or_404(CaseCollection, pk=exam_id)
|
||||||
@@ -2227,7 +2339,7 @@ def combine_series(request):
|
|||||||
it will not migrate other models such as SeriesFinding or other details
|
it will not migrate other models such as SeriesFinding or other details
|
||||||
"""
|
"""
|
||||||
series_ids = request.POST.getlist("series-ids")
|
series_ids = request.POST.getlist("series-ids")
|
||||||
#series = get_object_or_404(Series, pk=series_id)
|
# series = get_object_or_404(Series, pk=series_id)
|
||||||
if len(series_ids) < 2:
|
if len(series_ids) < 2:
|
||||||
return HttpResponse("Not enough series to combine")
|
return HttpResponse("Not enough series to combine")
|
||||||
|
|
||||||
@@ -2252,7 +2364,8 @@ def combine_series(request):
|
|||||||
series.delete()
|
series.delete()
|
||||||
|
|
||||||
return HttpResponse(f"Series {series_ids} combines")
|
return HttpResponse(f"Series {series_ids} combines")
|
||||||
#return HttpResponse("Fail")
|
# return HttpResponse("Fail")
|
||||||
|
|
||||||
|
|
||||||
class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
|
class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
|
||||||
model = CaseCollection
|
model = CaseCollection
|
||||||
@@ -2264,6 +2377,7 @@ class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView)
|
|||||||
context["collection"] = context["object"]
|
context["collection"] = context["object"]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CaseAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
|
class CaseAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
|
||||||
model = Case
|
model = Case
|
||||||
form_class = CaseAuthorForm
|
form_class = CaseAuthorForm
|
||||||
@@ -2272,4 +2386,4 @@ class CaseAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(CaseAuthorUpdate, self).get_context_data(**kwargs)
|
context = super(CaseAuthorUpdate, self).get_context_data(**kwargs)
|
||||||
context["collection"] = context["object"]
|
context["collection"] = context["object"]
|
||||||
return context
|
return context
|
||||||
|
|||||||
@@ -837,41 +837,6 @@ input {
|
|||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select2-dropdown {
|
|
||||||
background-color: #05668d;
|
|
||||||
color: white;
|
|
||||||
border-color: #05668d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-selection .select2-selection--multiple,
|
|
||||||
.select2 .select2-selection--single,
|
|
||||||
.select2-selection {
|
|
||||||
background-color: #05668d;
|
|
||||||
color: white;
|
|
||||||
/* border: 1px solid #aaa; */
|
|
||||||
border-radius: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-results__option {
|
|
||||||
background-color: #05668d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-search__field {
|
|
||||||
background-color: #05668d;
|
|
||||||
color: darkblue;
|
|
||||||
border: 1px solid black;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-container--default.select2-container--focus .select2-selection--multiple,
|
|
||||||
.select2-container--default.select2-container--default .select2-selection--multiple,
|
|
||||||
.select2-selection .select2-selection__rendered .select2-selection__choice {
|
|
||||||
background-color: #5897fb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select2-selection__rendered {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temp-thumb-large {
|
.temp-thumb-large {
|
||||||
width: 1000px;
|
width: 1000px;
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
form .helptext {
|
||||||
|
color: #666;
|
||||||
|
float: right;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
form>div:hover .helptext {
|
||||||
|
color: #666;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
form > div {
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form > div:hover {
|
||||||
|
border: 1px solid #666;
|
||||||
|
margin: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
form>div>label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form label,
|
||||||
|
form legend {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
form>div>fieldset>legend {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
form input:required {
|
||||||
|
border: 1px darkblue dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list_formset li {
|
||||||
|
border-bottom: 1px gray dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-dropdown {
|
||||||
|
background-color: #05668d;
|
||||||
|
color: white;
|
||||||
|
border-color: #05668d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-selection .select2-selection--multiple,
|
||||||
|
.select2 .select2-selection--single,
|
||||||
|
.select2-selection {
|
||||||
|
background-color: #05668d;
|
||||||
|
background-color: #5897fb;
|
||||||
|
color: white;
|
||||||
|
/* border: 1px solid #aaa; */
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-results__option {
|
||||||
|
background-color: #05668d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list_formset li {
|
||||||
|
border-bottom: 1px gray dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list_formset li:has(input[name*='DELETE']:checked) {
|
||||||
|
border: 1px dashed red;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-search__field {
|
||||||
|
background-color: #05668d;
|
||||||
|
color: darkblue;
|
||||||
|
border: 1px solid black;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-container--default.select2-container--focus .select2-selection--multiple,
|
||||||
|
.select2-container--default.select2-container--default .select2-selection--multiple,
|
||||||
|
.select2-selection .select2-selection__rendered .select2-selection__choice {
|
||||||
|
background-color: #5897fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select2-selection__rendered {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
{% block js %}
|
{% block js %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" href="{% static 'css/anatomy.css' %}">
|
<link rel="stylesheet" href="{% static 'css/anatomy.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/forms.css' %}">
|
||||||
{% block css %}
|
{% block css %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<table>
|
<table>
|
||||||
{{ form.as_table }}
|
{{ form }}
|
||||||
</table>
|
</table>
|
||||||
<input type="submit" value="Submit">
|
<input type="submit" value="Submit">
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user