Add advanced case search page with filtering options and pagination
This commit is contained in:
@@ -49,6 +49,9 @@
|
|||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'atlas:normals_list' %}"><i class="bi bi-file-earmark-check me-1" aria-hidden="true"></i>Normals</a>
|
<a class="dropdown-item" href="{% url 'atlas:normals_list' %}"><i class="bi bi-file-earmark-check me-1" aria-hidden="true"></i>Normals</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{% url 'atlas:case_search_page' %}"><i class="bi bi-search me-1" aria-hidden="true"></i>Search Cases</a>
|
||||||
|
</li>
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{% extends 'atlas/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<h2>Search Cases</h2>
|
||||||
|
|
||||||
|
<form method="get" class="row g-2 mb-3" id="case-search-form">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input type="search" name="q" value="{{ q }}" class="form-control" placeholder="Search cases...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<select name="field" class="form-select">
|
||||||
|
<option value="all" {% if field == 'all' %}selected{% endif %}>All fields</option>
|
||||||
|
<option value="title" {% if field == 'title' %}selected{% endif %}>Title</option>
|
||||||
|
<option value="description" {% if field == 'description' %}selected{% endif %}>Description</option>
|
||||||
|
<option value="history" {% if field == 'history' %}selected{% endif %}>History</option>
|
||||||
|
<option value="author" {% if field == 'author' %}selected{% endif %}>Author</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2">
|
||||||
|
<select name="subspecialty" class="form-select">
|
||||||
|
<option value="">Any subspecialty</option>
|
||||||
|
{% for s in subspecialty_queryset %}
|
||||||
|
<option value="{{ s.pk }}" {% if selected_subspecialty and selected_subspecialty|stringformat:"s" == s.pk|stringformat:"s" %}selected{% endif %}>{{ s.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2 d-flex gap-2">
|
||||||
|
<label class="form-check-label me-2"><input type="checkbox" name="my" value="1" class="form-check-input" {% if my %}checked{% endif %}> My cases</label>
|
||||||
|
<label class="form-check-label me-2"><input type="checkbox" name="open" value="1" class="form-check-input" {% if open %}checked{% endif %}> Open access</label>
|
||||||
|
<label class="form-check-label"><input type="checkbox" name="has_images" value="1" class="form-check-input" {% if has_images %}checked{% endif %}> Has images</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-primary">Search</button>
|
||||||
|
<a href="{% url 'atlas:case_search_page' %}" class="btn btn-outline-secondary ms-2">Clear</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="case-search-results">
|
||||||
|
{% include 'atlas/partials/case_search_results.html' with cases=cases recent_cases=recent_cases collection=None q=q %}
|
||||||
|
|
||||||
|
{# pagination controls #}
|
||||||
|
{% if cases.has_other_pages %}
|
||||||
|
<nav aria-label="Case results pagination">
|
||||||
|
<ul class="pagination mt-3">
|
||||||
|
{% if cases.has_previous %}
|
||||||
|
<li class="page-item"><a class="page-link" href="?{% if q %}q={{ q|urlencode }}&{% endif %}{% if field %}field={{ field }}&{% endif %}{% if my %}my=1&{% endif %}{% if open %}open=1&{% endif %}{% if has_images %}has_images=1&{% endif %}{% if selected_subspecialty %}subspecialty={{ selected_subspecialty }}&{% endif %}page={{ cases.previous_page_number }}">Previous</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled"><span class="page-link">Previous</span></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="page-item disabled"><span class="page-link">Page {{ cases.number }} of {{ cases.paginator.num_pages }}</span></li>
|
||||||
|
|
||||||
|
{% if cases.has_next %}
|
||||||
|
<li class="page-item"><a class="page-link" href="?{% if q %}q={{ q|urlencode }}&{% endif %}{% if field %}field={{ field }}&{% endif %}{% if my %}my=1&{% endif %}{% if open %}open=1&{% endif %}{% if has_images %}has_images=1&{% endif %}{% if selected_subspecialty %}subspecialty={{ selected_subspecialty }}&{% endif %}page={{ cases.next_page_number }}">Next</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled"><span class="page-link">Next</span></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
hx-trigger="input delay:500ms, keyup[key=='Enter']"
|
hx-trigger="input delay:500ms, keyup[key=='Enter']"
|
||||||
hx-target="#case-search-results"
|
hx-target="#case-search-results"
|
||||||
hx-indicator="#case-search-indicator" />
|
hx-indicator="#case-search-indicator" />
|
||||||
|
<a class="btn btn-outline-secondary ms-2" href="{% url 'atlas:case_search_page' %}" title="Open advanced case search">Advanced</a>
|
||||||
<span class="input-group-text" id="case-search-indicator" aria-hidden="true">
|
<span class="input-group-text" id="case-search-indicator" aria-hidden="true">
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
<span class="visually-hidden">Loading</span>
|
<span class="visually-hidden">Loading</span>
|
||||||
|
|||||||
@@ -9,11 +9,15 @@
|
|||||||
<div class="small text-muted">{% if case.description %}{{ case.description|truncatechars:100 }}{% endif %}</div>
|
<div class="small text-muted">{% if case.description %}{{ case.description|truncatechars:100 }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if collection %}
|
||||||
<form class="m-0" hx-post="{% url 'atlas:add_case_to_collection' collection.pk %}" hx-target="#full-question-list" hx-swap="beforeend">
|
<form class="m-0" hx-post="{% url 'atlas:add_case_to_collection' collection.pk %}" hx-target="#full-question-list" hx-swap="beforeend">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="case" value="{{ case.pk }}">
|
<input type="hidden" name="case" value="{{ case.pk }}">
|
||||||
<button type="submit" class="btn btn-sm btn-primary">Add</button>
|
<button type="submit" class="btn btn-sm btn-primary">Add</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:case_displaysets' case.pk %}">View</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -33,11 +37,15 @@
|
|||||||
<div class="fw-semibold">{{ case.title }}</div>
|
<div class="fw-semibold">{{ case.title }}</div>
|
||||||
<div class="small text-muted">{% if case.description %}{{ case.description|truncatechars:100 }}{% endif %}</div>
|
<div class="small text-muted">{% if case.description %}{{ case.description|truncatechars:100 }}{% endif %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if collection %}
|
||||||
<form class="m-0" hx-post="{% url 'atlas:add_case_to_collection' collection.pk %}" hx-target="#full-question-list" hx-swap="beforeend">
|
<form class="m-0" hx-post="{% url 'atlas:add_case_to_collection' collection.pk %}" hx-target="#full-question-list" hx-swap="beforeend">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="case" value="{{ case.pk }}">
|
<input type="hidden" name="case" value="{{ case.pk }}">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-primary">Add</button>
|
<button type="submit" class="btn btn-sm btn-outline-primary">Add</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'atlas:case_displaysets' case.pk %}">View</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -421,6 +421,7 @@ urlpatterns = [
|
|||||||
path("categories/", views.categories_list, name="categories_list"),
|
path("categories/", views.categories_list, name="categories_list"),
|
||||||
path("categories/search_partial/", views.categories_search_partial, name="categories_search_partial"),
|
path("categories/search_partial/", views.categories_search_partial, name="categories_search_partial"),
|
||||||
path("search/cases/", views.case_search_partial, name="case_search_partial"),
|
path("search/cases/", views.case_search_partial, name="case_search_partial"),
|
||||||
|
path("search/cases/page/", views.case_search_page, name="case_search_page"),
|
||||||
path("normals/", views.NormalCaseList.as_view(), name="normals_list"),
|
path("normals/", views.NormalCaseList.as_view(), name="normals_list"),
|
||||||
path("condition/<int:pk>", views.condition_detail, name="condition_detail"),
|
path("condition/<int:pk>", views.condition_detail, name="condition_detail"),
|
||||||
path(
|
path(
|
||||||
|
|||||||
@@ -345,6 +345,104 @@ def case_displaysets(request, pk):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def case_search_page(request):
|
||||||
|
"""Full page case search with quick filters.
|
||||||
|
|
||||||
|
Supports GET params:
|
||||||
|
- q: query string
|
||||||
|
- field: one of ['all','title','description','history','author']
|
||||||
|
- my: '1' to limit to cases authored by current user
|
||||||
|
- open: '1' to limit to open_access cases
|
||||||
|
- has_images: '1' to require series images
|
||||||
|
- subspecialty: id of subspecialty to filter
|
||||||
|
- page: pagination page
|
||||||
|
"""
|
||||||
|
q = request.GET.get("q", "").strip()
|
||||||
|
field = request.GET.get("field", "all")
|
||||||
|
my_cases = request.GET.get("my", "0") == "1"
|
||||||
|
open_access = request.GET.get("open", "0") == "1"
|
||||||
|
has_images = request.GET.get("has_images", "0") == "1"
|
||||||
|
subspecialty_id = request.GET.get("subspecialty")
|
||||||
|
|
||||||
|
qs = get_cases_available_to_user(request.user)
|
||||||
|
|
||||||
|
if my_cases:
|
||||||
|
qs = qs.filter(author=request.user)
|
||||||
|
|
||||||
|
if open_access:
|
||||||
|
qs = qs.filter(open_access=True)
|
||||||
|
|
||||||
|
if subspecialty_id:
|
||||||
|
try:
|
||||||
|
sid = int(subspecialty_id)
|
||||||
|
qs = qs.filter(subspecialty__id=sid)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if q:
|
||||||
|
if field == "title":
|
||||||
|
qs = qs.filter(title__icontains=q)
|
||||||
|
elif field == "description":
|
||||||
|
qs = qs.filter(description__icontains=q)
|
||||||
|
elif field == "history":
|
||||||
|
qs = qs.filter(history__icontains=q)
|
||||||
|
elif field == "author":
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(author__first_name__icontains=q)
|
||||||
|
| Q(author__last_name__icontains=q)
|
||||||
|
| Q(author__username__icontains=q)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(title__icontains=q)
|
||||||
|
| Q(description__icontains=q)
|
||||||
|
| Q(history__icontains=q)
|
||||||
|
| Q(discussion__icontains=q)
|
||||||
|
| Q(report__icontains=q)
|
||||||
|
| Q(condition__name__icontains=q)
|
||||||
|
| Q(presentation__name__icontains=q)
|
||||||
|
| Q(subspecialty__name__icontains=q)
|
||||||
|
| Q(series__description__icontains=q)
|
||||||
|
| Q(series__modality__modality__icontains=q)
|
||||||
|
| Q(series__examination__examination__icontains=q)
|
||||||
|
| Q(author__first_name__icontains=q)
|
||||||
|
| Q(author__last_name__icontains=q)
|
||||||
|
| Q(author__username__icontains=q)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
if has_images:
|
||||||
|
qs = qs.filter(series__images__isnull=False).distinct()
|
||||||
|
|
||||||
|
qs = qs.order_by("-created_date")
|
||||||
|
|
||||||
|
# Paginate results
|
||||||
|
paginator = Paginator(qs, 25)
|
||||||
|
page_number = request.GET.get("page")
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
# Recent cases (authored by current user)
|
||||||
|
recent_cases = get_cases_available_to_user(request.user).filter(author=request.user).order_by("-created_date")[:5]
|
||||||
|
|
||||||
|
subspecialty_queryset = Subspecialty.objects.all().order_by("name")
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"atlas/case_search_page.html",
|
||||||
|
{
|
||||||
|
"cases": page_obj,
|
||||||
|
"q": q,
|
||||||
|
"field": field,
|
||||||
|
"my": my_cases,
|
||||||
|
"open": open_access,
|
||||||
|
"has_images": has_images,
|
||||||
|
"subspecialty_queryset": subspecialty_queryset,
|
||||||
|
"selected_subspecialty": subspecialty_id,
|
||||||
|
"recent_cases": recent_cases,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_has_case_view_access
|
@user_has_case_view_access
|
||||||
@require_http_methods(["GET", "POST"])
|
@require_http_methods(["GET", "POST"])
|
||||||
|
|||||||
Reference in New Issue
Block a user