diff --git a/atlas/models.py b/atlas/models.py index fc734e9a..03d63482 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -1443,6 +1443,13 @@ class CaseDetail(models.Model): def __str__(self) -> str: return f"{self.case} -> {self.collection}" + + def get_case_index(self): + """Returns the question index (number) in the collection (0 indexed)""" + case_details = list(self.collection.casedetail_set.all().order_by("sort_order")) + + return case_details.index(self) + def get_question_schema(self): return json.dumps(self.question_schema) diff --git a/atlas/templates/atlas/collection_case_displaysetup.html b/atlas/templates/atlas/collection_case_displaysetup.html index 88871f51..8283c7e4 100644 --- a/atlas/templates/atlas/collection_case_displaysetup.html +++ b/atlas/templates/atlas/collection_case_displaysetup.html @@ -30,7 +30,7 @@ document.getElementById('save-viewerstate').addEventListener('click', function() document.getElementById('viewerstate-save-response').innerHTML = "Viewer state could not be retrieved."; return; } - fetch("{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk casedetail.case.pk %}", { + fetch({% if case_number %}"{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk case_number=case_number %}"{% else %}"{% url 'atlas:collection_case_displaysetup_legacy' casedetail.collection.pk casedetail.case.pk %}"{% endif %}, { method: "POST", headers: { "Content-Type": "application/json", @@ -47,7 +47,7 @@ document.getElementById('save-viewerstate').addEventListener('click', function() }); }); document.getElementById('reset-viewerstate').addEventListener('click', function() { - fetch("{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk casedetail.case.pk %}", { + fetch({% if case_number %}"{% url 'atlas:collection_case_displaysetup' casedetail.collection.pk case_number=case_number %}"{% else %}"{% url 'atlas:collection_case_displaysetup_legacy' casedetail.collection.pk casedetail.case.pk %}"{% endif %}, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/atlas/templates/atlas/collection_case_view_take.html b/atlas/templates/atlas/collection_case_view_take.html index c6e53381..e0f11093 100644 --- a/atlas/templates/atlas/collection_case_view_take.html +++ b/atlas/templates/atlas/collection_case_view_take.html @@ -66,9 +66,9 @@
{% if question_completed %} - + {% else %} - + {% endif %}
@@ -77,10 +77,10 @@ {% if collection.show_ohif_viewer_link %}
- {% if question_completed %} - + {% if question_completed %} + {% else %} - + {% endif %}
{% endif %} diff --git a/atlas/templates/atlas/partials/_prior_card.html b/atlas/templates/atlas/partials/_prior_card.html index cbefa7d5..a8e18b7c 100644 --- a/atlas/templates/atlas/partials/_prior_card.html +++ b/atlas/templates/atlas/partials/_prior_card.html @@ -24,9 +24,14 @@ View case - {% if not added %} -
+ {% if not added %} + {% csrf_token %}
@@ -44,8 +49,13 @@
{% else %} -
+ {% csrf_token %}
Current relation: {{relation}}
diff --git a/atlas/templates/atlas/partials/_viewing_case_as_part_of_collection.html b/atlas/templates/atlas/partials/_viewing_case_as_part_of_collection.html index eaecffc7..12c4d64e 100644 --- a/atlas/templates/atlas/partials/_viewing_case_as_part_of_collection.html +++ b/atlas/templates/atlas/partials/_viewing_case_as_part_of_collection.html @@ -2,7 +2,7 @@
{% if previous %} - ← Previous + ← Previous {% endif %}
@@ -14,7 +14,7 @@
{% if next %} - Next → + Next → {% endif %}
diff --git a/atlas/templates/atlas/partials/casedetails_management_links.html b/atlas/templates/atlas/partials/casedetails_management_links.html index 57f71e10..24aecf31 100644 --- a/atlas/templates/atlas/partials/casedetails_management_links.html +++ b/atlas/templates/atlas/partials/casedetails_management_links.html @@ -1,14 +1,14 @@
- + - + {% if collection.collection_type == "QUE" %} - + {% if casedetail.question_schema %} {% else %} diff --git a/atlas/urls.py b/atlas/urls.py index 4eace63f..00197e45 100755 --- a/atlas/urls.py +++ b/atlas/urls.py @@ -168,30 +168,56 @@ urlpatterns = [ ), path("exam//groups", views.ExamGroupsUpdate.as_view(), name="exam_groups_edit"), path( - "collection//case//details", + "collection//case//details", views.collection_case_details, name="collection_case_details", ), + # Backwards-compatible redirect from legacy case_id-style URLs path( - "collection//case//questions", + "collection//case//details", + views.redirect_collection_case_details_by_id, + name="collection_case_details_legacy", + ), + path( + "collection//case//questions", views.collection_case_questions, name="collection_case_questions", ), path( - "collection//case//priors", + "collection//case//questions", + views.redirect_collection_case_questions_by_id, + name="collection_case_questions_legacy", + ), + path( + "collection//case//priors", views.collection_case_priors, name="collection_case_priors", ), path( - "collection//case//dicom_json", + "collection//case//priors", + views.redirect_collection_case_priors_by_id, + name="collection_case_priors_legacy", + ), + path( + "collection//case//dicom_json", views.collection_case_dicom_json, name="collection_case_dicom_json", ), path( - "collection//case//dicom_json/review", + "collection//case//dicom_json", + views.redirect_collection_case_dicom_json_by_id, + name="collection_case_dicom_json_legacy", + ), + path( + "collection//case//dicom_json/review", views.collection_case_dicom_json_review, name="collection_case_dicom_json_review", ), + path( + "collection//case//dicom_json/review", + views.redirect_collection_case_dicom_json_review_by_id, + name="collection_case_dicom_json_review_legacy", + ), path( "collection//question_schemas", views.collection_question_schemas, @@ -272,26 +298,52 @@ urlpatterns = [ views.collection_case_view, name="collection_case_view", ), + # legacy path that used case PK; redirect to canonical case_number URL path( - "collection///display_setup", + "collection//", + views.redirect_collection_case_view_by_id, + name="collection_case_view_legacy", + ), + path( + "collection///display_setup", views.collection_case_displaysetup, name="collection_case_displaysetup", ), + path( + "collection///display_setup", + views.redirect_collection_case_displaysetup_by_id, + name="collection_case_displaysetup_legacy", + ), path( "collection///take//", views.collection_case_view_take, name="collection_case_view_take", ), + path( + "collection///take//", + views.redirect_collection_case_view_take_by_id, + name="collection_case_view_take_legacy", + ), path( "collection///take/", views.collection_case_view_take_user, name="collection_case_view_take_user", ), + path( + "collection///take/", + views.redirect_collection_case_view_take_user_by_id, + name="collection_case_view_take_user_legacy", + ), path( "collection///take/answers", views.collection_case_view_take_user_answers, name="collection_case_view_take_user_answers", ), + path( + "collection///take/answers", + views.redirect_collection_case_view_take_user_answers_by_id, + name="collection_case_view_take_user_answers_legacy", + ), path( "collection//json_edit", views.GenericExamViews.exam_json_edit, diff --git a/atlas/views.py b/atlas/views.py index 234ced35..6d2b0b11 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -2755,10 +2755,101 @@ def collection_take_start(request, pk, cid=None, passcode=None): ) +def _case_number_for_collection_and_caseid(collection_pk, case_id): + """Helper: given a collection PK and a case PK, return the case index (case_number). + + Raises Http404 if the case is not found in the collection. + """ + collection = get_object_or_404(CaseCollection, pk=collection_pk) + try: + # attempt to resolve the case object (may raise Http404) + case_obj = get_case_for_case_detail(case_id) + except Http404: + raise + try: + return collection.get_index_of_case(case_obj) + except ValueError: + raise Http404(f"Case {case_id} not in collection {collection_pk}") + + +@login_required +def redirect_collection_case_details_by_id(request, exam_id, case_id): + case_number = _case_number_for_collection_and_caseid(exam_id, case_id) + return redirect("atlas:collection_case_details", exam_id=exam_id, case_number=case_number) + + +@login_required +def redirect_collection_case_questions_by_id(request, exam_id, case_id): + case_number = _case_number_for_collection_and_caseid(exam_id, case_id) + return redirect("atlas:collection_case_questions", exam_id=exam_id, case_number=case_number) + + +@login_required +def redirect_collection_case_priors_by_id(request, exam_id, case_id): + case_number = _case_number_for_collection_and_caseid(exam_id, case_id) + return redirect("atlas:collection_case_priors", exam_id=exam_id, case_number=case_number) + + +@login_required +def redirect_collection_case_dicom_json_by_id(request, exam_id, case_id): + case_number = _case_number_for_collection_and_caseid(exam_id, case_id) + return redirect("atlas:collection_case_dicom_json", exam_id=exam_id, case_number=case_number) + + +@login_required +def redirect_collection_case_dicom_json_review_by_id(request, exam_id, case_id): + case_number = _case_number_for_collection_and_caseid(exam_id, case_id) + return redirect("atlas:collection_case_dicom_json_review", exam_id=exam_id, case_number=case_number) + + +@login_required +def redirect_collection_case_view_by_id(request, pk, case_id): + case_number = _case_number_for_collection_and_caseid(pk, case_id) + return redirect("atlas:collection_case_view", pk=pk, case_number=case_number) + + +@login_required +def redirect_collection_case_displaysetup_by_id(request, collection_id, case_id): + case_number = _case_number_for_collection_and_caseid(collection_id, case_id) + return redirect("atlas:collection_case_displaysetup", collection_id=collection_id, case_number=case_number) + + +@login_required +def redirect_collection_case_view_take_by_id(request, pk, case_id, cid, passcode): + case_number = _case_number_for_collection_and_caseid(pk, case_id) + return redirect("atlas:collection_case_view_take", pk=pk, case_number=case_number, cid=cid, passcode=passcode) + + +@login_required +def redirect_collection_case_view_take_user_by_id(request, pk, case_id): + case_number = _case_number_for_collection_and_caseid(pk, case_id) + return redirect("atlas:collection_case_view_take_user", pk=pk, case_number=case_number) + + +@login_required +def redirect_collection_case_view_take_user_answers_by_id(request, pk, case_id): + case_number = _case_number_for_collection_and_caseid(pk, case_id) + return redirect("atlas:collection_case_view_take_user_answers", pk=pk, case_number=case_number) + + + @user_is_collection_author_or_atlas_editor def collection_case_priors(request, exam_id, case_id): - casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id) - collection = casedetail.collection + # Support either case_number (index) or case_id (case PK) in the URL/kwargs. + # Backwards compatibility: callers may pass case_id; prefer case_number when present. + casedetail = None + collection = None + # Try treating the parameter as a case PK first, falling back to case index. + try: + casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id) + collection = casedetail.collection + except Exception: + collection = get_object_or_404(CaseCollection, pk=exam_id) + try: + case_obj = collection.get_case_by_index(int(case_id)) + casedetail = CaseDetail.objects.get(case=case_obj, collection=collection) + except Exception: + raise Http404("Case not found in collection") if request.htmx: # Ensure we can render the updated single-card partial and return it so HTMX @@ -2892,9 +2983,17 @@ def collection_case_priors(request, exam_id, case_id): @user_is_collection_author_or_atlas_editor def collection_case_questions(request, exam_id, case_id): - casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id) - - collection = casedetail.collection + # Support either case_number (index) or case_id (case PK) + try: + casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id) + collection = casedetail.collection + except Exception: + collection = get_object_or_404(CaseCollection, pk=exam_id) + try: + case_obj = collection.get_case_by_index(int(case_id)) + casedetail = CaseDetail.objects.get(case=case_obj, collection=collection) + except Exception: + raise Http404("Case not found in collection") if not collection.collection_type == "QUE": raise Http404("Collection not in question mode") @@ -3005,9 +3104,19 @@ def collection_case_questions(request, exam_id, case_id): @user_is_collection_author_or_atlas_editor def collection_case_details(request, exam_id, case_id): - casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id) - - collection = casedetail.collection + # Accept either case_number (index into collection) or case_id (case PK). + casedetail = None + collection = None + try: + casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id) + collection = casedetail.collection + except Exception: + collection = get_object_or_404(CaseCollection, pk=exam_id) + try: + case_obj = collection.get_case_by_index(int(case_id)) + casedetail = CaseDetail.objects.get(case=case_obj, collection=collection) + except Exception: + raise Http404("Case not found in collection") if request.method == "POST": form = CaseDetailForm(request.POST, instance=casedetail) @@ -3303,12 +3412,14 @@ def collection_case_view_take_answers( ) -def collection_case_view_take_user(request, pk: int, case_number: int): - return collection_case_view_take(request, pk, case_number) +def collection_case_view_take_user(request, pk, case_number=None, case_id=None): + # Accept either case_number (index) or case_id (case PK) and forward both to + # the handler. This allows URLs that use either name to keep working. + return collection_case_view_take(request, pk, case_number=case_number, cid=None, passcode=None, case_id=case_id) def collection_case_view_take( - request, pk: int, case_number: int, cid=None, passcode=None + request, pk, case_number=None, cid=None, passcode=None, case_id=None ): """ Displays the page from which the user takes a case in the collection @@ -3328,6 +3439,21 @@ def collection_case_view_take( """ collection = get_object_or_404(CaseCollection, pk=pk) + # If caller provided a case_id (case PK) instead of a case_number (index), + # translate it into the index used by collection.get_case_by_index(). + if case_id is not None and case_number is None: + try: + case_obj = get_case_for_case_detail(case_id) + except Http404: + raise + try: + case_number = collection.get_index_of_case(case_obj) + except ValueError: + raise Http404(f"Case {case_id} not in collection {pk}") + # Ensure we have a case_number to work with + if case_number is None: + raise Http404("No case specified") + form = None answer: None | CidReportAnswer | UserReportAnswer = None @@ -3567,7 +3693,7 @@ def collection_case_view_take( @user_is_collection_author_or_atlas_editor -def collection_case_view(request, pk, case_number): +def collection_case_view(request, pk, case_number=None, case_id=None): """Edit/management overview of the case (as part of the collection) Args: @@ -3583,6 +3709,19 @@ def collection_case_view(request, pk, case_number): """ collection = get_object_or_404(CaseCollection, pk=pk) + # Support either case_number (index) or case_id (case PK) in the URL/kwargs. + if case_id is not None and case_number is None: + try: + case_obj = get_case_for_case_detail(case_id) + except Http404: + raise + try: + case_number = collection.get_index_of_case(case_obj) + except ValueError: + raise Http404(f"Case {case_id} not in collection {pk}") + if case_number is None: + raise Http404("No case specified") + form = None answer = None @@ -3635,8 +3774,18 @@ def use_dates_as_descriptions(request, pk): def collection_case_dicom_json_review(request, exam_id, case_id): return collection_case_dicom_json(request, exam_id, case_id, review=True) + def collection_case_dicom_json(request, exam_id, case_id, review=False): - casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id) + # Support either case_number (index) or case_id (case PK) + try: + casedetail = CaseDetail.objects.get(case=case_id, collection=exam_id) + except Exception: + collection = get_object_or_404(CaseCollection, pk=exam_id) + try: + case_obj = collection.get_case_by_index(int(case_id)) + casedetail = CaseDetail.objects.get(case=case_obj, collection=collection) + except Exception: + raise Http404("Case not found in collection") if review: priors = casedetail.caseprior_set.exclude(prior_visibility="NO") @@ -4517,12 +4666,26 @@ def series_bulk_delete(request): @user_is_collection_author_or_atlas_editor @csrf_exempt # Only if you have CSRF issues; otherwise, keep CSRF protection -def collection_case_displaysetup(request, collection_id, case_id): +def collection_case_displaysetup(request, collection_id, case_number=None, case_id=None): """ View to set up the display options for a specific case in a collection. This can include setting which series to display, annotations, etc. """ - casedetail = get_object_or_404(CaseDetail, case__pk=case_id, collection__pk=collection_id) + # Accept either case_number (index into collection) or case_id (case PK). + casedetail = None + try: + # If a case_id was passed explicitly, try that first + if case_id is not None: + casedetail = get_object_or_404(CaseDetail, case__pk=case_id, collection__pk=collection_id) + else: + # Otherwise the route most likely supplied case_number; try resolving by index + collection = get_object_or_404(CaseCollection, pk=collection_id) + case_obj = collection.get_case_by_index(int(case_number)) + casedetail = CaseDetail.objects.get(case=case_obj, collection=collection) + except Http404: + raise + except Exception: + raise Http404("Case not found in collection") if request.method == "POST": data = json.loads(request.body.decode("utf-8"))