max improvement

This commit is contained in:
Ross
2026-07-04 23:43:18 +01:00
parent 55fcf71431
commit bb6b4c625c
13 changed files with 217 additions and 36 deletions
+10 -1
View File
@@ -1495,12 +1495,21 @@ class CaseSeriesForm(forms.ModelForm):
def __init__(self, *args, queryset, user=None, is_atlas_editor=False, **kwargs):
super().__init__(*args, **kwargs)
self.fields["series"].queryset = queryset
self.fields["series"].label_from_instance = lambda obj: str(obj.pk)
self.fields["series"].label_from_instance = lambda obj: f"Series {obj.pk}: {obj.description or ''} ({obj.modality or ''})"
class Meta:
model = SeriesDetail # through model for Case.series
fields = ["series", "sort_order", "feedback"]
widgets = {
"series": autocomplete.ModelSelect2(
url="atlas:series-autocomplete",
attrs={
"data-placeholder": "Search by Series ID or description...",
"data-minimum-input-length": 0,
}
)
}
class CaseUpdateSeriesForm(forms.ModelForm):
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE HTML>
{% load static %}
{% load django_htmx %}
{% load crispy_forms_tags %}
<html data-bs-theme="dark">
<head>
<title>Create Resource</title>
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
<link href="{% static 'css/widgets.css' %}" type="text/css" media="all" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/bootstrap-icons.css' %}">
<script src="{% static 'js/jquery-3.7.1.min.js' %}"></script>
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
{% block js %}
{% endblock %}
<link rel="stylesheet" href="{% static 'css/forms.css' %}">
</head>
<body class="p-3">
{% block content %}
{% endblock %}
</body>
</html>
+1 -1
View File
@@ -35,7 +35,7 @@
<a href="{% url 'atlas:case_detail' case.pk %}" style="color: inherit; text-decoration: none;">View Case</a>
</button>
<button class="edit-link-case btn btn-outline-warning btn-sm ms-2" style="display:none;">
<a href="{% url 'atlas:case_update' case.pk %}" style="color: inherit; text-decoration: none;">Edit Case</a>
<a href="{% url 'atlas:case_update' case.pk %}?next={{ request.get_full_path|urlencode }}" style="color: inherit; text-decoration: none;">Edit Case</a>
</button>
<br />
<div class="pre-whitespace multi-image-block">
@@ -48,7 +48,7 @@
<a href="{% url 'atlas:case_detail' case.pk %}" style="color: inherit; text-decoration: none;">View Case</a>
</button>
<button class="edit-link-case btn btn-outline-warning btn-sm" style="display:none;">
<a href="{% url 'atlas:case_update' case.pk %}" style="color: inherit; text-decoration: none;">Edit Case</a>
<a href="{% url 'atlas:case_update' case.pk %}?next={{ request.get_full_path|urlencode }}" style="color: inherit; text-decoration: none;">Edit Case</a>
</button>
</div>
</header>
@@ -0,0 +1,21 @@
{% load crispy_forms_tags %}
<form hx-post="{% url 'atlas:case_create_resource_inline' pk=case.pk %}"
hx-encoding="multipart/form-data"
hx-target="#case-resources-list"
hx-swap="innerHTML"
class="bg-dark p-3 rounded border border-secondary text-white my-3">
{% csrf_token %}
<h5>Create New Resource</h5>
{{ form|crispy }}
<div class="form-check mb-3">
<input type="checkbox" name="pre_review" class="form-check-input" id="id_pre_review">
<label class="form-check-label" for="id_pre_review">Pre-review (show before case is taken)</label>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-sm btn-success">Save and Attach</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
hx-get="{% url 'atlas:resource_search_panel' %}?case_id={{ case.pk }}"
hx-target="#resource-search-panel-placeholder"
hx-swap="innerHTML">Cancel</button>
</div>
</form>
@@ -35,6 +35,14 @@
<label class="form-check-label small" for="resource-open-access-{{ case.pk }}">Open</label>
</div>
</div>
<div class="col-auto ms-auto">
<button type="button" class="btn btn-sm btn-outline-success"
hx-get="{% url 'atlas:case_create_resource_inline' pk=case.pk %}"
hx-target="#resource-search-panel"
hx-swap="outerHTML">
+ New Resource
</button>
</div>
</div>
<div id="resource-search-results" class="mt-2"></div>
@@ -2,7 +2,7 @@
<div class="btn-toolbar" role="toolbar" aria-label="Case actions">
<div class="btn-group me-2 mb-2" role="group" aria-label="Primary actions">
<a class="btn btn-sm btn-outline-primary" href="{% url 'atlas:case_detail' pk=case.pk %}" title="View the Case">View</a>
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:case_update' pk=case.pk %}" title="Edit the Case">Edit</a>
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:case_update' pk=case.pk %}?next={{ request.get_full_path|urlencode }}" title="Edit the Case">Edit</a>
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:case_series_update' pk=case.pk %}" title="Edit the Cases Series">Series</a>
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:case_displaysets' pk=case.pk %}" title="View and edit case display sets">Display sets</a>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends "atlas/base.html" %}
{% extends base_template|default:"atlas/base.html" %}
<!-- {% load static from static %} -->
{% load crispy_forms_tags %}
+9
View File
@@ -5,6 +5,15 @@
<h2>Resources</h2>
<button class="btn btn-primary mb-3">
<a href="{% url 'atlas:resource_create' %}">Create New Resource</a>
</button>
{% comment %} <details>
<summary>Search Resources</summary>
{% include "atlas/partials/resource_search_panel.html" %}
</details>
{% endcomment %}
<form>
{% render_table table %}
</form>
+7 -1
View File
@@ -2,7 +2,7 @@ from django.urls import path, include
from django.views.generic import RedirectView, TemplateView
from atlas.models import CaseCollection, Condition, Finding, Presentation, Structure, Subspecialty, Procedure
from atlas.models import CaseCollection, Condition, Finding, Presentation, Structure, Subspecialty, Procedure, Series
from . import views
app_name = "atlas"
@@ -254,6 +254,7 @@ urlpatterns = [
path("case/<int:pk>/attach-resource/", views.case_attach_resource, name="case_attach_resource"),
path("case/<int:pk>/detach-resource/", views.case_detach_resource, name="case_detach_resource"),
path("case/<int:pk>/toggle-resource-pre/", views.case_toggle_resource_pre, name="case_toggle_resource_pre"),
path("case/<int:pk>/create-resource-inline/", views.case_create_resource_inline, name="case_create_resource_inline"),
# Backwards-compatible redirect from legacy case_id-style URLs
path(
"collection/<int:exam_id>/case/id/<int:case_id>/details",
@@ -785,6 +786,11 @@ urlpatterns = [
views.SubspecialtyAutocomplete.as_view(model=Subspecialty, create_field="name"),
name="subspecialty-autocomplete",
),
path(
"series-autocomplete",
views.SeriesAutocomplete.as_view(model=Series),
name="series-autocomplete",
),
path("presentation/", views.PresentationView.as_view(), name="presentation_view"),
path(
"presentation/<int:pk>", views.presentation_detail, name="presentation_detail"
+127 -29
View File
@@ -4301,11 +4301,28 @@ class ResourceCreate(LoginRequiredMixin, CreateView):
model = Resource
form_class = ResourceForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.GET.get("popup") == "1":
context["base_template"] = "atlas/base_popup.html"
else:
context["base_template"] = "atlas/base.html"
return context
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.save()
form.instance.author.add(self.request.user.id)
if self.request.GET.get("popup") == "1":
field_id = self.request.GET.get("field_id", "")
from django.utils.html import escapejs
return HttpResponse(
f'<script type="text/javascript">'
f'opener.closePopup(window, "{self.object.pk}", "{escapejs(str(self.object))}", "#{field_id}");'
f'</script>'
)
return super().form_valid(form)
@@ -4637,14 +4654,15 @@ class AtlasCreateBase(RevisionMixin, LoginRequiredMixin):
kwargs.update({"user": self.request.user})
return kwargs
def get_success_url(self):
next_url = self.request.GET.get("next")
if next_url:
return next_url
return self.object.get_absolute_url()
def get_context_data(self, **kwargs):
context = super(AtlasCreateBase, self).get_context_data(**kwargs)
if self.request.POST:
# context["series_formset"] = SeriesFormSet(
# self.request.POST,
# self.request.FILES,
# form_kwargs={"user": self.request.user},
# )
context["casedifferential_formset"] = CaseDifferentialFormSet(
self.request.POST, self.request.FILES
)
@@ -4653,18 +4671,20 @@ class AtlasCreateBase(RevisionMixin, LoginRequiredMixin):
self.request.FILES,
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=Case.objects.filter(author__id=self.request.user.id), queryset=Series.objects.filter(author__id=self.request.user.id))
# context["series_formset"] = SeriesFormSet(
# form_kwargs={"user": self.request.user}
# )
context["casedifferential_formset"] = CaseDifferentialFormSet()
context["caseresource_formset"] = CaseResourceFormSet(
form_kwargs={"user": self.request.user}
)
next_url = self.request.GET.get("next")
if not next_url:
if self.object and self.object.pk:
next_url = self.object.get_absolute_url()
else:
next_url = reverse("atlas:case_view")
context["next_url"] = next_url
return context
def form_valid(self, form):
@@ -4760,15 +4780,15 @@ class AtlasUpdate(
kwargs.update({"user": self.request.user})
return kwargs
def get_success_url(self):
next_url = self.request.GET.get("next")
if next_url:
return next_url
return self.object.get_absolute_url()
def get_context_data(self, **kwargs):
context = super(AtlasUpdate, self).get_context_data(**kwargs)
if self.request.POST:
# context["series_formset"] = SeriesFormSet(
# self.request.POST,
# self.request.FILES,
# instance=self.object,
# form_kwargs={"user": self.request.user},
# )
context["casedifferential_formset"] = CaseDifferentialFormSet(
self.request.POST, self.request.FILES, instance=self.object
)
@@ -4778,13 +4798,9 @@ class AtlasUpdate(
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}
# )
context["casedifferential_formset"] = CaseDifferentialFormSet(
instance=self.object
)
@@ -4792,6 +4808,13 @@ class AtlasUpdate(
instance=self.object,
form_kwargs={"user": self.request.user},
)
next_url = self.request.GET.get("next")
if not next_url:
if self.object and self.object.pk:
next_url = self.object.get_absolute_url()
else:
next_url = reverse("atlas:case_view")
context["next_url"] = next_url
return context
def form_valid(self, form):
@@ -4888,15 +4911,7 @@ class AtlasSeriesUpdate(
queryset = Series.objects.filter(author__id=self.request.user.id)
else:
queryset = Series.objects.all()
queryset = queryset.select_related("modality").prefetch_related(
"examination",
"plane",
"contrast",
Prefetch(
"images",
queryset=SeriesImage.objects.filter(removed=False).order_by("position"),
),
)
queryset = queryset.select_related("modality")
if self.request.POST:
context["series_formset"] = SeriesFormSet(
@@ -6216,6 +6231,32 @@ class SubspecialtyAutocomplete(autocomplete.Select2QuerySetView):
return qs
class SeriesAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_authenticated:
return Series.objects.none()
qs = Series.objects.all().select_related("modality")
is_atlas_editor = (
self.request.user.groups.filter(name="atlas_editor").exists()
or self.request.user.is_superuser
)
if not is_atlas_editor:
qs = qs.filter(author__id=self.request.user.id)
if self.q:
q_filter = Q(description__icontains=self.q) | Q(info__icontains=self.q)
if self.q.isdigit():
q_filter |= Q(pk=int(self.q))
qs = qs.filter(q_filter)
return qs
def get_result_label(self, result):
return f"Series {result.pk}: {result.description or ''} ({result.modality or ''})"
@login_required
# @user_is_atlas_editor
def categories_list(request):
@@ -9871,6 +9912,63 @@ def case_toggle_resource_pre(request, pk):
return resp
@login_required
def case_create_resource_inline(request, pk):
"""Create a new resource and attach it to the case (HTMX).
GET: Renders the inline form `ResourceForm`.
POST: Processes the form, saves new Resource, sets author, attaches to case,
and returns updated `atlas/partials/case_resources_list.html` partial.
"""
case = get_case_for_case_detail(pk)
if not case.check_user_can_edit(request.user):
return HttpResponse(status=403)
if request.method == "POST":
form = ResourceForm(request.POST, request.FILES)
if form.is_valid():
resource = form.save(commit=False)
resource.save()
resource.author.add(request.user.id)
pre_review = request.POST.get("pre_review") == "on" or request.POST.get("pre_review") == "1"
CaseResource.objects.create(
case=case,
resource=resource,
pre_review=pre_review,
)
# Refresh case from DB to render updated resources list
try:
case = Case.objects.get(pk=case.pk)
except Exception:
pass
html = render_to_string(
"atlas/partials/case_resources_list.html", {"case": case}, request=request
)
add_resource_button_html = (
f'<div id="resource-search-panel-placeholder" hx-swap-oob="true" class="d-inline-flex">'
f' <button class="btn btn-sm btn-outline-primary" '
f' hx-get="{reverse("atlas:resource_search_panel")}?case_id={case.pk}" '
f' hx-target="#resource-search-panel-placeholder" '
f' hx-swap="innerHTML"> '
f' Add Resource '
f' </button>'
f'</div>'
)
resp = HttpResponse(html + add_resource_button_html)
resp["HX-Trigger"] = json.dumps({"caseResourcesChanged": {"case_id": case.pk}})
return resp
else:
form = ResourceForm()
return render(
request,
"atlas/partials/case_create_resource_inline.html",
{"form": form, "case": case},
)
def series_dicom_json(request, pk):
series = get_object_or_404(Series, pk=pk)
+1 -1
View File
@@ -4,4 +4,4 @@ set -e
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
docker compose -f docker/docker-compose.prod.yml -f docker/docker-compose.test.yml -f docker/docker-compose.dev.yml down --volumes --remove-orphans
docker compose -f docker/docker-compose.prod.yml -f docker/docker-compose.local.yml -f docker/docker-compose.test.yml -f docker/docker-compose.dev.yml down --volumes --remove-orphans
+9
View File
@@ -123,6 +123,15 @@ else
fi
fi
# Stop and clean up any running containers/networks to prevent conflicts (especially if local db/cache are running)
echo "Stopping any existing compose services to prevent network conflicts..."
COMPOSE_ENV=$COMPOSE_ENV docker compose \
-f docker/docker-compose.prod.yml \
-f docker/docker-compose.local.yml \
-f docker/docker-compose.test.yml \
-f docker/docker-compose.dev.yml \
down || true
if [ "$NO_BUILD" -eq 1 ]; then
COMPOSE_ENV=$COMPOSE_ENV docker compose -f docker/docker-compose.prod.yml -f docker/docker-compose.dev.yml up
else