max improvement
This commit is contained in:
+10
-1
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
@@ -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,4 +1,4 @@
|
||||
{% extends "atlas/base.html" %}
|
||||
{% extends base_template|default:"atlas/base.html" %}
|
||||
<!-- {% load static from static %} -->
|
||||
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user