i should break these commits up

This commit is contained in:
Ross
2024-04-08 14:16:13 +01:00
parent f13d1ba7e2
commit 98756baa37
25 changed files with 839 additions and 366 deletions
+31 -7
View File
@@ -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,
) )
+10
View File
@@ -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)
@@ -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)'),
),
]
+58 -24
View File
@@ -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")
@@ -383,6 +398,9 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
# def get_edit_template_links(self): # def get_edit_template_links(self):
def __str__(self): def __str__(self):
if self.open_access:
return f"{self.pk}: {self.title} [OA]"
else:
return f"{self.pk}: {self.title}" return f"{self.pk}: {self.title}"
def get_series_blocks(self): def get_series_blocks(self):
@@ -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",
@@ -668,6 +685,7 @@ class Series(SeriesBase):
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,9 +879,7 @@ 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",
@@ -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,11 +911,7 @@ 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"""
@@ -907,7 +920,6 @@ class CaseCollection(ExamOrCollectionGenericBase):
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")):
@@ -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>
+2
View File
@@ -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 %}
+10 -2
View File
@@ -1,6 +1,5 @@
<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" }}
@@ -26,6 +25,15 @@
<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 %}
<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> <div class="pre-whitespace multi-image-block"><b>Series:</b>
<form> <form>
+39 -29
View File
@@ -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 %}
+21 -12
View File
@@ -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 %}
<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> <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>
+7 -2
View File
@@ -8,7 +8,7 @@
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,8 +21,13 @@
{% endfor %} {% endfor %}
</ol> </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> <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>
@@ -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 %}
+4
View File
@@ -3,7 +3,11 @@
{% block navigation %} {% block navigation %}
{{block.super}} {{block.super}}
{% if collection %}
{% include "atlas/collection_headers.html" %} {% 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> /
+32
View File
@@ -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 %}
+26
View File
@@ -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>
+21
View File
@@ -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 %}
+1 -1
View File
@@ -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 }}
+24
View File
@@ -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,
+143 -29
View File
@@ -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):
@@ -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())
@@ -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:
@@ -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,6 +1464,7 @@ 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)
@@ -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)
@@ -1409,7 +1497,9 @@ 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)
@@ -2254,6 +2366,7 @@ def combine_series(request):
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
form_class = CaseCollectionAuthorForm form_class = CaseCollectionAuthorForm
@@ -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
-35
View File
@@ -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;
+89
View File
@@ -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;
}
+1
View File
@@ -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>
+1 -1
View File
@@ -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>