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.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,
)
+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)'),
),
]
+64 -30
View File
@@ -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>
+2
View File
@@ -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 %}
+17 -9
View File
@@ -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>
+39 -29
View File
@@ -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 %}
+21 -12
View File
@@ -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>
+9 -4
View File
@@ -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 %}
+4
View File
@@ -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> /
+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 %}
{{ form.non_field_errors }}
{{ form.as_p }}
{{ form }}
{% comment %} <div class="fieldWrapper">
{{ form.content_type.errors }}
{{ 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/<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
View File
@@ -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
-35
View File
@@ -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;
+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 %}
{% endblock %}
<link rel="stylesheet" href="{% static 'css/anatomy.css' %}">
<link rel="stylesheet" href="{% static 'css/forms.css' %}">
{% block css %}
{% endblock %}
</head>
+1 -1
View File
@@ -10,7 +10,7 @@
<form action="" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
{{ form }}
</table>
<input type="submit" value="Submit">
</form>