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.forms import (
|
||||
BaseInlineFormSet,
|
||||
BooleanField,
|
||||
Form,
|
||||
HiddenInput,
|
||||
ModelForm,
|
||||
@@ -54,6 +55,11 @@ import logging
|
||||
|
||||
from generic.forms import ExamAuthorFormMixin
|
||||
|
||||
from crispy_forms.helper import FormHelper
|
||||
|
||||
from atlas.helpers import get_cases_available_to_user
|
||||
|
||||
|
||||
class ConditionForm(ModelForm):
|
||||
class Meta:
|
||||
model = Condition
|
||||
@@ -75,7 +81,7 @@ class ConditionForm(ModelForm):
|
||||
class CaseCollectionForm(ModelForm):
|
||||
class Meta:
|
||||
model = CaseCollection
|
||||
exclude = ["cases", "valid_cid_users", "author"]
|
||||
exclude = ["cases", "valid_cid_users", "valid_user_users", "author"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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))
|
||||
|
||||
|
||||
class Media:
|
||||
# Django also includes a few javascript files necessary
|
||||
# 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()]
|
||||
|
||||
|
||||
|
||||
ModelForm.__init__(self, *args, **kwargs)
|
||||
|
||||
super(CaseForm, self).__init__(*args, **kwargs)
|
||||
|
||||
# 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):
|
||||
return (
|
||||
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):
|
||||
|
||||
def __init__(self, *args, user, **kwargs):
|
||||
@@ -417,6 +441,9 @@ class CaseResourceForm(ModelForm):
|
||||
# widget=Select(verbose_name="Series", is_stacked=False),
|
||||
)
|
||||
|
||||
self.fields["pre_review"] = BooleanField(required=False)
|
||||
|
||||
|
||||
class CaseSeriesForm(ModelForm):
|
||||
|
||||
def __init__(self, *args, user, **kwargs):
|
||||
@@ -440,10 +467,7 @@ class CaseCollectionCaseForm(ModelForm):
|
||||
def __init__(self, *args, user, **kwargs):
|
||||
super(CaseCollectionCaseForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if not user.groups.filter(name="atlas_editor").exists():
|
||||
queryset = Case.objects.filter(author__id=user.id)
|
||||
else:
|
||||
queryset = Case.objects.all()
|
||||
queryset = get_cases_available_to_user(user)
|
||||
|
||||
self.fields["case"] = ModelChoiceField(
|
||||
required=False,
|
||||
@@ -478,7 +502,7 @@ CaseResourceFormSet = inlineformset_factory(
|
||||
form=CaseResourceForm,
|
||||
exclude=[],
|
||||
can_delete=True,
|
||||
extra=0,
|
||||
extra=1,
|
||||
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)'),
|
||||
),
|
||||
]
|
||||
+64
-30
@@ -311,20 +311,32 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
||||
# findings = models.TextField(null=True, blank=True)
|
||||
|
||||
# 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)
|
||||
|
||||
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(
|
||||
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)
|
||||
@@ -352,7 +364,9 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
||||
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)
|
||||
|
||||
@@ -362,6 +376,7 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
||||
null=True,
|
||||
related_name="next_case",
|
||||
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")
|
||||
@@ -378,11 +393,14 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
||||
# return f"{self.pk}: {self.title}"
|
||||
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):
|
||||
if self.open_access:
|
||||
return f"{self.pk}: {self.title} [OA]"
|
||||
else:
|
||||
return f"{self.pk}: {self.title}"
|
||||
|
||||
def get_series_blocks(self):
|
||||
@@ -396,7 +414,7 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
||||
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)}]"
|
||||
|
||||
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 = []
|
||||
for series in self.series.all():
|
||||
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())
|
||||
|
||||
|
||||
|
||||
def extract_image_dicom_json_from_ds(ds, url, image_index):
|
||||
to_keep = [
|
||||
"Columns",
|
||||
@@ -597,7 +614,7 @@ class Series(SeriesBase):
|
||||
"<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):
|
||||
instances = []
|
||||
|
||||
@@ -668,6 +685,7 @@ class Series(SeriesBase):
|
||||
|
||||
self.save()
|
||||
|
||||
|
||||
class CaseCollection(ExamOrCollectionGenericBase):
|
||||
app_name = "atlas"
|
||||
|
||||
@@ -806,9 +824,13 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
||||
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 (
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -843,9 +865,7 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
||||
) -> Case | Tuple[Case, int]:
|
||||
# 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)
|
||||
cases = list(
|
||||
self.get_cases().prefetch_related()
|
||||
)
|
||||
cases = list(self.get_cases().prefetch_related())
|
||||
|
||||
try:
|
||||
case = cases[case_index]
|
||||
@@ -859,16 +879,14 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
||||
return case
|
||||
|
||||
def get_case_take_url(self, case):
|
||||
cases = list(
|
||||
self.get_cases().prefetch_related()
|
||||
)
|
||||
cases = list(self.get_cases().prefetch_related())
|
||||
|
||||
return reverse(
|
||||
"atlas:collection_case_view_take_user",
|
||||
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 = []
|
||||
for n, case in enumerate(self.cases.all()):
|
||||
series_json = []
|
||||
@@ -880,7 +898,6 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
||||
patient_name = case.title
|
||||
|
||||
studies.append(
|
||||
|
||||
{
|
||||
"StudyInstanceUID": f"1.3.6.1.4.1.14519.5.2.1.6279.6001.{n}",
|
||||
"StudyDate": "20000101",
|
||||
@@ -894,20 +911,15 @@ class CaseCollection(ExamOrCollectionGenericBase):
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
return {
|
||||
"studies": studies
|
||||
}
|
||||
return {"studies": studies}
|
||||
|
||||
def add_case(self, case):
|
||||
"""Adds a case to the collection and makes sure order is maintained"""
|
||||
# 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.order_cases()
|
||||
|
||||
|
||||
def order_cases(self):
|
||||
"""Modifies the casedetail sort_order to sequentially order the cases"""
|
||||
for n, c in enumerate(self.casedetail_set.all().order_by("sort_order")):
|
||||
@@ -921,11 +933,15 @@ class SeriesDetail(models.Model):
|
||||
|
||||
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:
|
||||
ordering = ("sort_order",)
|
||||
|
||||
|
||||
class CaseDetail(models.Model):
|
||||
case = models.ForeignKey(Case, 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)]
|
||||
)
|
||||
|
||||
completed = models.BooleanField(default=False)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
return super(BaseReportAnswer, self).save(*args, **kwargs)
|
||||
@@ -1145,6 +1163,7 @@ class UncategorisedDicom(models.Model):
|
||||
class DuplicateDicom(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Resource(models.Model, AuthorMixin):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
@@ -1166,11 +1185,26 @@ class Resource(models.Model, AuthorMixin):
|
||||
def get_link(self):
|
||||
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):
|
||||
resource = models.ForeignKey(Resource, 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:
|
||||
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">
|
||||
<form hx-post="{% url 'atlas:add_case_to_collection' collection.id %}" hx-swap="outerHTML">
|
||||
{% csrf_token %}
|
||||
<label for="case">Select Case:</label>
|
||||
<label for="case">Select Case to add:</label>
|
||||
<select name="case" id="case">
|
||||
{% for case in cases %}
|
||||
<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:series_view' %}">Series</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: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:help' %}" title="View the atlas help pages">Help</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<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>
|
||||
<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 class="date">
|
||||
{{ case.created_date|date:"d/m/Y" }}
|
||||
@@ -26,6 +25,15 @@
|
||||
<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>Resource</b><br/>{% for caseresource in case.caseresource_set.all %}
|
||||
<a href='{% url "atlas:resource_detail" caseresource.resource.pk %}'>{{caseresource.resource.name}}</a>
|
||||
{% if caseresource.pre_review %}
|
||||
<span title="This resource will be avalible to review when or taking a case">(pre)</span>
|
||||
{% else %}
|
||||
<span title="This resource will only be available when reviewing a completed case">(post)</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
||||
<div class="pre-whitespace multi-image-block"><b>Series:</b>
|
||||
<form>
|
||||
@@ -167,16 +175,16 @@
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<p><b>Checked by:</b> {% for verified in case.verified.all %} <a
|
||||
<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>Author(s):</b> {% for author in case.author.all %} <a
|
||||
<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() {
|
||||
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);
|
||||
}
|
||||
|
||||
function add_differential_input_form() {
|
||||
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);
|
||||
}
|
||||
function add_resource_input_form() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -60,72 +60,82 @@
|
||||
{% endif %}
|
||||
|
||||
<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 %}
|
||||
<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 %}
|
||||
|
||||
<form action="" method="post" enctype="multipart/form-data" id="atlas-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
{{ form }}
|
||||
<h3>Series:</h3>
|
||||
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">
|
||||
<div id="series_formset">
|
||||
<div id="series_formset" class="list_formset">
|
||||
<ol>
|
||||
{% for form in series_formset %}
|
||||
<ul class="no-error series-formset">
|
||||
<li class="no-error series-formset">
|
||||
{{form.non_field_errors}}
|
||||
{{form.errors}}
|
||||
{{ form.as_ul }}
|
||||
</ul>
|
||||
{{ form }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
{{ series_formset.management_form }}
|
||||
<h3>Differential:</h3>
|
||||
Add differential here.
|
||||
<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 %}
|
||||
<ul class="no-error casedifferential_formset">
|
||||
<li class="no-error casedifferential_formset">
|
||||
{{form.non_field_errors}}
|
||||
{{form.errors}}
|
||||
{{ form.as_ul }}
|
||||
</ul>
|
||||
{{ form }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
{{ casedifferential_formset.management_form }}
|
||||
<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">
|
||||
<div id="caseresource_formset">
|
||||
<div id="caseresource_formset" class="list_formset">
|
||||
<ol>
|
||||
{% for form in caseresource_formset %}
|
||||
<ul class="no-error caseresource_formset">
|
||||
<li class="no-error caseresource_formset">
|
||||
{{form.non_field_errors}}
|
||||
{{form.errors}}
|
||||
{{ form.as_ul }}
|
||||
</ul>
|
||||
{{ form }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
{{ caseresource_formset.management_form }}
|
||||
<br/>
|
||||
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||
</form>
|
||||
<div id="empty_series_form" style="display:none">
|
||||
<ul class='no_error series-formset'>
|
||||
{{ series_formset.empty_form.as_ul }}
|
||||
</ul>
|
||||
<li class='no_error series-formset'>
|
||||
{{ series_formset.empty_form }}
|
||||
</li>
|
||||
</div>
|
||||
<div id="empty_casedifferential_form" style="display:none">
|
||||
<ul class='no_error casedifferential_formset'>
|
||||
{{ casedifferential_formset.empty_form.as_ul }}
|
||||
</ul>
|
||||
<li class='no_error casedifferential_formset'>
|
||||
{{ casedifferential_formset.empty_form }}
|
||||
</li>
|
||||
</div>
|
||||
<div id="empty_caseresource_form" style="display:none">
|
||||
<ul class='no_error caseresource_formset'>
|
||||
{{ caseresource_formset.empty_form.as_ul }}
|
||||
</ul>
|
||||
<li class='no_error caseresource_formset'>
|
||||
{{ caseresource_formset.empty_form }}
|
||||
</li>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "atlas/base.html" %}
|
||||
{% extends "atlas/exams.html" %}
|
||||
<!-- {% load static from static %} -->
|
||||
|
||||
{% block css %}
|
||||
@@ -10,7 +10,7 @@
|
||||
<script type="text/javascript">
|
||||
function add_case_input_form() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -23,34 +23,43 @@
|
||||
<!-- {{ form.media }} -->
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% if collection %}
|
||||
<h2>Update Collection</h2>
|
||||
|
||||
{% else %}
|
||||
<h2>Create Collection</h2>
|
||||
Use this form to create a collection of cases
|
||||
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post" enctype="multipart/form-data" id="atlas-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
</table>
|
||||
{{ form }}
|
||||
<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.
|
||||
<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" />
|
||||
<div id="case_formset" class="sortable">
|
||||
<div id="case_formset" class="sortable list_formset">
|
||||
<ol>
|
||||
{% for form in case_formset %}
|
||||
<ul class="no-error case-formset">
|
||||
<li class="no-error case-formset">
|
||||
{{form.non_field_errors}}
|
||||
{{form.errors}}
|
||||
{{ form.as_ul }}
|
||||
</ul>
|
||||
{{ form }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
{{ case_formset.management_form }}
|
||||
<input type="submit" class="submit-button" value="Submit" name="submit">
|
||||
</form>
|
||||
<div id="empty_case_form" style="display:none">
|
||||
<ul class='no_error case-formset'>
|
||||
{{ case_formset.empty_form.as_ul }}
|
||||
</ul>
|
||||
<li class='no_error case-formset'>
|
||||
{{ case_formset.empty_form }}
|
||||
</li>
|
||||
</div>
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
|
||||
@@ -16,6 +16,20 @@
|
||||
|
||||
</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 %}
|
||||
<div>
|
||||
Description: {{case.description}}
|
||||
@@ -28,14 +42,18 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if collection.show_ohif_viewer_link %}
|
||||
{% if collection.show_ohif_viewer %}
|
||||
<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>
|
||||
{% 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 %}
|
||||
<div class="pre-whitespace multi-image-block">
|
||||
@@ -89,8 +107,19 @@
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if collection.publish_results or cid_user_exam.completed %}
|
||||
{% if completed %}
|
||||
<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 %}
|
||||
<form method="POST" class="post-form">{% csrf_token %}
|
||||
@@ -133,6 +162,11 @@
|
||||
{% 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 />
|
||||
<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>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{% extends 'atlas/exams.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{{collection.name}}</h2>
|
||||
<h2>{{collection.name}}</h2>
|
||||
|
||||
{% include 'exam_notes.html' %}
|
||||
{% include 'exam_notes.html' %}
|
||||
<div>
|
||||
Exam mode: {{collection.exam_mode}}<br />
|
||||
Publish results: {{collection.publish_results}}<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 />
|
||||
</div>
|
||||
<h3>Cases</h3>
|
||||
@@ -21,8 +21,13 @@
|
||||
{% endfor %}
|
||||
</ol>
|
||||
|
||||
<p>This collection will be available to view/take <a href='{{collection.get_take_url}}'>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 %}
|
||||
<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>
|
||||
|
||||
@@ -27,14 +27,23 @@
|
||||
{% url 'atlas:collection_case_view_take_user' pk=collection.id case_number=forloop.counter0 %}
|
||||
{% 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>
|
||||
<br/>
|
||||
{% if not collection.review_only %}
|
||||
{% if not answer %}
|
||||
No answer
|
||||
{% else %}
|
||||
<details><summary>Answered</summary>
|
||||
{{answer.answer}}
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</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.super}}
|
||||
|
||||
|
||||
{% if collection %}
|
||||
{% include "atlas/collection_headers.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% 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_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 %}
|
||||
{{ form.non_field_errors }}
|
||||
|
||||
{{ form.as_p }}
|
||||
{{ form }}
|
||||
{% comment %} <div class="fieldWrapper">
|
||||
{{ form.content_type.errors }}
|
||||
{{ form.content_type }}
|
||||
|
||||
@@ -42,6 +42,15 @@ urlpatterns = [
|
||||
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/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(
|
||||
"collection/create",
|
||||
views.CaseCollectionCreate.as_view(),
|
||||
@@ -74,6 +83,21 @@ urlpatterns = [
|
||||
views.GenericExamViews.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(
|
||||
"exam/<int:exam_id>/cids/edit",
|
||||
views.GenericExamViews.exam_cids_edit,
|
||||
|
||||
+159
-45
@@ -31,6 +31,7 @@ from django.http import Http404, JsonResponse
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
|
||||
from generic.models import CidUser, CidUserExam
|
||||
from atlas.helpers import get_cases_available_to_user
|
||||
|
||||
from .forms import (
|
||||
AddCollectionToCaseForm,
|
||||
@@ -45,6 +46,7 @@ from .forms import (
|
||||
ConditionAutocompleteForm,
|
||||
ConditionForm,
|
||||
FindingForm,
|
||||
ResourceForm,
|
||||
SelfReviewForm,
|
||||
SeriesForm,
|
||||
SeriesImageFormSet,
|
||||
@@ -63,6 +65,7 @@ from .models import (
|
||||
Differential,
|
||||
PathologicalProcess,
|
||||
Presentation,
|
||||
Resource,
|
||||
SelfReview,
|
||||
Series,
|
||||
Examination,
|
||||
@@ -127,7 +130,12 @@ import logging
|
||||
from copy import deepcopy
|
||||
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
|
||||
import reversion
|
||||
|
||||
@@ -139,14 +147,6 @@ import difflib
|
||||
|
||||
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):
|
||||
def get_object(self, *args, **kwargs):
|
||||
@@ -160,6 +160,7 @@ class AuthorOrCheckerRequiredMixin(object):
|
||||
raise PermissionDenied() # or Http404
|
||||
return obj
|
||||
|
||||
|
||||
class AtlasEditorRequiredMixin(object):
|
||||
def get_object(self, *args, **kwargs):
|
||||
obj = super().get_object(*args, **kwargs)
|
||||
@@ -170,6 +171,7 @@ class AtlasEditorRequiredMixin(object):
|
||||
return obj
|
||||
raise PermissionDenied("You must be an atlas editor to do this.") # or Http404
|
||||
|
||||
|
||||
@login_required
|
||||
@user_has_case_view_access
|
||||
def case_detail(request, pk):
|
||||
@@ -208,7 +210,7 @@ def series_detail(request, pk, finding_pk=None):
|
||||
|
||||
|
||||
@login_required
|
||||
#@user_is_atlas_editor
|
||||
# @user_is_atlas_editor
|
||||
def condition_detail(request, pk):
|
||||
condition = get_object_or_404(Condition, pk=pk)
|
||||
|
||||
@@ -216,7 +218,10 @@ def condition_detail(request, pk):
|
||||
|
||||
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
|
||||
|
||||
# logging.debug(atlas.subspecialty.first().name.all())
|
||||
@@ -275,7 +280,7 @@ def condition_merge(request, pk_to_merge):
|
||||
|
||||
|
||||
@login_required
|
||||
#@user_is_atlas_editor
|
||||
# @user_is_atlas_editor
|
||||
def subspecialty_detail(request, pk):
|
||||
subspecialty = get_object_or_404(Subspecialty, pk=pk)
|
||||
|
||||
@@ -290,7 +295,7 @@ def subspecialty_detail(request, pk):
|
||||
|
||||
|
||||
@login_required
|
||||
#@user_is_atlas_editor
|
||||
# @user_is_atlas_editor
|
||||
def presentation_detail(request, pk):
|
||||
presentation = get_object_or_404(Presentation, pk=pk)
|
||||
|
||||
@@ -305,7 +310,7 @@ def presentation_detail(request, pk):
|
||||
|
||||
|
||||
@login_required
|
||||
#@user_is_atlas_editor
|
||||
# @user_is_atlas_editor
|
||||
def pathological_process_detail(request, pk):
|
||||
pathological_process = get_object_or_404(PathologicalProcess, pk=pk)
|
||||
|
||||
@@ -320,7 +325,7 @@ def pathological_process_detail(request, pk):
|
||||
|
||||
|
||||
@login_required
|
||||
#@user_is_atlas_editor
|
||||
# @user_is_atlas_editor
|
||||
def structure_detail(request, pk):
|
||||
structure = get_object_or_404(Structure, pk=pk)
|
||||
|
||||
@@ -335,7 +340,7 @@ def structure_detail(request, pk):
|
||||
|
||||
|
||||
@login_required
|
||||
#@user_is_atlas_editor
|
||||
# @user_is_atlas_editor
|
||||
def finding_detail(request, 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
|
||||
def author_detail(request, pk):
|
||||
# logging.debug(Author.objects.all())
|
||||
@@ -381,11 +393,11 @@ def user_collections(request):
|
||||
|
||||
return render(request, "atlas/user_collections.html", {"collections": collections})
|
||||
|
||||
|
||||
def add_case_to_collection(request, collection_id):
|
||||
if not request.htmx:
|
||||
return Http404
|
||||
|
||||
|
||||
collection = get_object_or_404(CaseCollection, pk=collection_id)
|
||||
|
||||
if request.method == "POST":
|
||||
@@ -395,14 +407,18 @@ def add_case_to_collection(request, collection_id):
|
||||
return HttpResponse("Case already in collection")
|
||||
print(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)
|
||||
|
||||
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
|
||||
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},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def all_uploads(request, case_id: int | None = None):
|
||||
return user_uploads(request, case_id, user_only=False)
|
||||
|
||||
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
if user_only:
|
||||
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()
|
||||
tags = d.basic_dicom_tags
|
||||
|
||||
|
||||
data[tags["SeriesInstanceUID"]].append((tags, d.created_date))
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||
|
||||
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):
|
||||
model = CaseCollection
|
||||
form_class = CaseCollectionForm
|
||||
template_name = "atlas/casecollection_form.html"
|
||||
|
||||
def get_initial(self):
|
||||
# print(self.request)
|
||||
@@ -553,7 +603,6 @@ class CaseCollectionCreate(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||
self.object.save()
|
||||
form.instance.author.add(self.request.user.id)
|
||||
|
||||
|
||||
context = self.get_context_data(form=form)
|
||||
case_formset = context["case_formset"]
|
||||
if case_formset.is_valid():
|
||||
@@ -618,7 +667,7 @@ class SeriesCreate(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||
class CaseCollectionUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView):
|
||||
model = CaseCollection
|
||||
form_class = CaseCollectionForm
|
||||
template_name = "atlas/collection_update_form.html"
|
||||
template_name = "atlas/casecollection_form.html"
|
||||
|
||||
# fields = '__all__'
|
||||
# #fields = [ 'condition' ]
|
||||
@@ -724,6 +773,18 @@ class SeriesUpdate(
|
||||
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):
|
||||
model = Condition
|
||||
form_class = ConditionForm
|
||||
@@ -775,7 +836,8 @@ class AtlasCreateBase(RevisionMixin, LoginRequiredMixin):
|
||||
self.request.POST, self.request.FILES
|
||||
)
|
||||
context["caseresource_formset"] = CaseResourceFormSet(
|
||||
self.request.POST, self.request.FILES,
|
||||
self.request.POST,
|
||||
self.request.FILES,
|
||||
form_kwargs={"user": self.request.user},
|
||||
)
|
||||
context["series_formset"].full_clean()
|
||||
@@ -805,7 +867,11 @@ class AtlasCreateBase(RevisionMixin, LoginRequiredMixin):
|
||||
series_formset = context["series_formset"]
|
||||
casedifferential_formset = context["casedifferential_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)
|
||||
series_formset.instance = self.object
|
||||
series_formset.save()
|
||||
@@ -871,8 +937,15 @@ class AtlasUpdate(
|
||||
context["casedifferential_formset"] = CaseDifferentialFormSet(
|
||||
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["casedifferential_formset"].full_clean()
|
||||
context["caseresource_formset"].full_clean()
|
||||
else:
|
||||
context["series_formset"] = SeriesFormSet(
|
||||
instance=self.object, form_kwargs={"user": self.request.user}
|
||||
@@ -880,6 +953,10 @@ class AtlasUpdate(
|
||||
context["casedifferential_formset"] = CaseDifferentialFormSet(
|
||||
instance=self.object
|
||||
)
|
||||
context["caseresource_formset"] = CaseResourceFormSet(
|
||||
instance=self.object,
|
||||
form_kwargs={"user": self.request.user},
|
||||
)
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
@@ -891,13 +968,20 @@ class AtlasUpdate(
|
||||
context = self.get_context_data(form=form)
|
||||
series_formset = context["series_formset"]
|
||||
casedifferential_formset = context["casedifferential_formset"]
|
||||
caseresource_formset = context["caseresource_formset"]
|
||||
# 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)
|
||||
series_formset.instance = self.object
|
||||
series_formset.save()
|
||||
casedifferential_formset.instance = self.object
|
||||
casedifferential_formset.save()
|
||||
caseresource_formset.instance = self.object
|
||||
caseresource_formset.save()
|
||||
|
||||
return response
|
||||
else:
|
||||
@@ -970,7 +1054,6 @@ class AtlasClone(AtlasCreateBase, AuthorOrCheckerRequiredMixin, CreateView):
|
||||
return initial_data
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@user_is_author_or_atlas_editor
|
||||
def atlas_scrap(request, pk):
|
||||
@@ -1069,6 +1152,7 @@ class FindingView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
|
||||
filterset_class = FindingFilter
|
||||
|
||||
|
||||
@login_required
|
||||
def case_order_dicom(request, pk):
|
||||
if request.htmx:
|
||||
@@ -1086,11 +1170,12 @@ def case_order_dicom(request, pk):
|
||||
fail.append(series.pk)
|
||||
|
||||
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:
|
||||
return HttpResponse("Series reordered")
|
||||
|
||||
|
||||
raise Http404
|
||||
|
||||
|
||||
@@ -1105,6 +1190,7 @@ def series_order_dicom(request, pk):
|
||||
|
||||
return redirect("atlas:series_detail", pk=pk)
|
||||
|
||||
|
||||
@login_required
|
||||
def image_diff(request):
|
||||
if request.htmx:
|
||||
@@ -1116,20 +1202,20 @@ def image_diff(request):
|
||||
second: SeriesImage = get_object_or_404(SeriesImage, pk=image_ids[1])
|
||||
|
||||
first_ds = pydicom.read_file(first.image.file)
|
||||
#del first_ds.PixelData
|
||||
#j1 = first_ds.to_json_dict()
|
||||
# del first_ds.PixelData
|
||||
# j1 = first_ds.to_json_dict()
|
||||
f = print_dicom(first_ds, join=False)
|
||||
|
||||
second_ds = pydicom.read_file(second.image.file)
|
||||
#del second_ds.PixelData
|
||||
#j2 = second_ds.to_json_dict()
|
||||
# del second_ds.PixelData
|
||||
# j2 = second_ds.to_json_dict()
|
||||
s = print_dicom(second_ds, join=False)
|
||||
|
||||
diff = difflib.HtmlDiff().make_table(f, s)
|
||||
|
||||
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}")
|
||||
|
||||
@@ -1142,7 +1228,6 @@ def image_diff(request):
|
||||
dict_diff = pprint.pformat(compare_dicom_datasets(*ds_set))
|
||||
return HttpResponse(f"<pre>{dict_diff}</pre>")
|
||||
|
||||
|
||||
return HttpResponse("Fail")
|
||||
|
||||
|
||||
@@ -1366,6 +1451,7 @@ class CollectionView(LoginRequiredMixin, SingleTableMixin, FilterView):
|
||||
|
||||
filterset_class = CaseCollectionFilter
|
||||
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
def collection_viva(request, 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},
|
||||
)
|
||||
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
def collection_viva_case(request, pk, case_number):
|
||||
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)
|
||||
|
||||
return render(
|
||||
@@ -1391,6 +1478,7 @@ def collection_viva_case(request, pk, case_number):
|
||||
{"collection": collection, "current_case": current_case},
|
||||
)
|
||||
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
def collection_viva_series(request, pk, series_id):
|
||||
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):
|
||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||
|
||||
#cases = collection.cases.all().order_by("casedetail__sort_order")
|
||||
casedetails = CaseDetail.objects.filter(collection=collection).order_by("sort_order")
|
||||
# cases = collection.cases.all().order_by("casedetail__sort_order")
|
||||
casedetails = CaseDetail.objects.filter(collection=collection).order_by(
|
||||
"sort_order"
|
||||
)
|
||||
|
||||
return render(
|
||||
request,
|
||||
@@ -1725,12 +1815,20 @@ def collection_case_view_take(
|
||||
else:
|
||||
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():
|
||||
answer = form.save(commit=False)
|
||||
answer.set_cid_or_user(cid=cid, user=request.user)
|
||||
answer.question = case_detail
|
||||
# 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()
|
||||
|
||||
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
|
||||
# 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
|
||||
show_title = collection.show_title_post
|
||||
show_history = collection.show_history_post
|
||||
@@ -1778,6 +1880,8 @@ def collection_case_view_take(
|
||||
show_report = collection.show_report_post
|
||||
|
||||
self_review = cid_user_exam.selfreview_set.filter(case=case)
|
||||
|
||||
resources = case.caseresource_set.all()
|
||||
else:
|
||||
completed = False
|
||||
show_title = collection.show_title_pre
|
||||
@@ -1787,6 +1891,8 @@ def collection_case_view_take(
|
||||
show_report = collection.show_report_pre
|
||||
self_review = []
|
||||
|
||||
resources = case.caseresource_set.filter(pre_review=True)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"atlas/collection_case_view_take.html",
|
||||
@@ -1806,6 +1912,7 @@ def collection_case_view_take(
|
||||
"show_description": show_description,
|
||||
"show_discussion": show_discussion,
|
||||
"show_report": show_report,
|
||||
"resources": resources,
|
||||
"cid_user_exam": cid_user_exam,
|
||||
"completed": completed,
|
||||
"self_review": self_review,
|
||||
@@ -1856,11 +1963,13 @@ def collection_case_view(request, pk, case_number):
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def collection_dicom_json(request, pk):
|
||||
collection = get_object_or_404(CaseCollection, pk=pk)
|
||||
|
||||
return JsonResponse(collection.get_ohif_dicom_json())
|
||||
|
||||
|
||||
@user_is_author_or_atlas_editor
|
||||
def use_dates_as_descriptions(request, pk):
|
||||
case = get_object_or_404(Case, pk=pk)
|
||||
@@ -1871,16 +1980,19 @@ def use_dates_as_descriptions(request, pk):
|
||||
|
||||
return HttpResponse("Done")
|
||||
|
||||
|
||||
def case_dicom_json(request, pk):
|
||||
case = get_object_or_404(Case, pk=pk)
|
||||
|
||||
return JsonResponse(case.get_case_dicom_json())
|
||||
|
||||
|
||||
def series_dicom_json(request, pk):
|
||||
series = get_object_or_404(Series, pk=pk)
|
||||
|
||||
return JsonResponse(series.get_ohif_dicom_json())
|
||||
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
def delete_collection_cid_answers(request, exam_id, cid):
|
||||
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
|
||||
"""
|
||||
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:
|
||||
return HttpResponse("Not enough series to combine")
|
||||
|
||||
@@ -2252,7 +2364,8 @@ def combine_series(request):
|
||||
series.delete()
|
||||
|
||||
return HttpResponse(f"Series {series_ids} combines")
|
||||
#return HttpResponse("Fail")
|
||||
# return HttpResponse("Fail")
|
||||
|
||||
|
||||
class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
|
||||
model = CaseCollection
|
||||
@@ -2264,6 +2377,7 @@ class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView)
|
||||
context["collection"] = context["object"]
|
||||
return context
|
||||
|
||||
|
||||
class CaseAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView):
|
||||
model = Case
|
||||
form_class = CaseAuthorForm
|
||||
|
||||
@@ -837,41 +837,6 @@ input {
|
||||
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 {
|
||||
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 %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" href="{% static 'css/anatomy.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/forms.css' %}">
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
{{ form }}
|
||||
</table>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user