Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make "Country" and "Source" columns sortable on Source List page #1592

Merged
merged 3 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions django/cantusdb_project/main_app/templates/source_list.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{% extends "base.html" %}
{% load helper_tags %}


{% block title %}
<title>Browse Sources | Cantus Database</title>
Expand Down Expand Up @@ -76,8 +78,8 @@ <h3>Browse Sources</h3>
<table class="table table-sm small table-bordered table-responsive">
<thead>
<tr>
<th scope="col" class="text-wrap" style="text-align:center">Country</th>
<th scope="col" class="text-wrap" style="text-align:center">Source</th>
{% sortable_header request "country" %}
{% sortable_header request "heading" "Source" %}
<th scope="col" class="text-wrap" style="text-align:center">Summary</th>
<th scope="col" class="text-wrap" style="text-align:center">Date/Origin</th>
<th scope="col" class="text-wrap" style="text-align:center">Image Link</th>
Expand All @@ -90,9 +92,9 @@ <h3>Browse Sources</h3>
<td class="text-wrap" style="text-align:center">
<b>{{ source.holding_institution.country }}</b>
</td>
<td class="text-wrap" style="text-align:center" title="{{ source.title }}">
<td class="text-wrap" style="text-align:center" title="{{ source.heading }}">
<a href="{% url 'source-detail' source.id %}">
<b>{{ source.title|truncatechars_html:100 }}</b>
<b>{{ source.heading|truncatechars_html:100 }}</b>
</a>
</td>
<td class="text-wrap" style="text-align:center" title="{{ source.summary|default:""|truncatechars_html:500 }}">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<th scope="col" class="text-wrap" style="text-align:center" title="{{ column_name }}">
{% if attr_is_currently_ordering %}
{% if current_sort_param == "desc" %}
<a href="{{ url_wo_sort_params }}&order={{ order_attribute }}&sort=asc">{{ column_name }}</a> ▼
{% else %}
<a href="{{ url_wo_sort_params }}&order={{ order_attribute }}&sort=desc">{{ column_name }}</a> ▲
{% endif %}
{% else %}
<a href="{{ url_wo_sort_params }}&order={{ order_attribute }}&sort=asc">{{ column_name }}</a>
{% endif %}
</th>
52 changes: 51 additions & 1 deletion django/cantusdb_project/main_app/templatetags/helper_tags.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import calendar
from typing import Union, Optional
from typing import Union, Optional, Any

from django import template
from django.core.paginator import Paginator
from django.db.models import Q
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
from django.http import HttpRequest

from articles.models import Article
from main_app.models import Source
Expand Down Expand Up @@ -193,3 +194,52 @@ def get_user_created_source_pagination(context):
page_number = context["request"].GET.get("page2")
user_created_sources_page_obj = paginator.get_page(page_number)
return user_created_sources_page_obj


@register.inclusion_tag("tag_templates/sortable_header.html")
def sortable_header(
request: HttpRequest,
order_attribute: str,
column_name: Optional[str] = None,
) -> dict[str, Union[str, bool, Optional[str]]]:
"""
A template tag for use in `ListView` templates or other templates that display
a table of model instances. This tag generates a table header (<th>) element
that, when clicked, sorts the table by the specified attribute.

params:
context: the current template-rendering context (passed by Django)
order_attribute: the attribute of the model that clicking the table header
should sort by
column_name: the user-facing name of the column (e.g. the text
of the <th> element). If None, use the camel-case version of
`sort_attribute`.

returns:
a dictionary containing the following
- order_attribute: the unchanged `order_attribute` parameter
- column_name: the user-facing name of the column (e.g. the value of `column_name`
or the camel-case version of `order_attribute`)
- attr_is_currently_ordering: a boolean indicating whether the table is currently
ordered by `order_attribute`
- current_sort_param: the current sort order (either "asc" or "desc")
- url_wo_sort_params: the current URL without sorting and pagination parameters
"""
current_order_param = request.GET.get("order")
current_sort_param = request.GET.get("sort")
# Remove order, sort, and page parameters from the query string
query_dict = request.GET.copy()
for param in ["order", "sort", "page"]:
if param in query_dict:
query_dict.pop(param)
# Create the current URL without sorting and pagination parameters
url_wo_sort_params = f"{request.path}?{query_dict.urlencode()}"
if column_name is None:
column_name = order_attribute.replace("_", " ").title()
return {
"order_attribute": order_attribute,
"column_name": column_name,
"attr_is_currently_ordering": order_attribute == current_order_param,
"current_sort_param": current_sort_param,
"url_wo_sort_params": url_wo_sort_params,
}
59 changes: 37 additions & 22 deletions django/cantusdb_project/main_app/views/source.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import UserPassesTestMixin
Expand Down Expand Up @@ -208,13 +210,13 @@ def get_context_data(self, **kwargs):
return context


class SourceListView(ListView):
class SourceListView(ListView): # type: ignore
model = Source
paginate_by = 100
context_object_name = "sources"
template_name = "source_list.html"

def get_context_data(self, **kwargs):
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
context["provenances"] = (
Provenance.objects.all().order_by("name").values("id", "name")
Expand All @@ -224,47 +226,45 @@ def get_context_data(self, **kwargs):
)
return context

def get_queryset(self):
def get_queryset(self) -> QuerySet[Source]:
# use select_related() for foreign keys to reduce DB queries
queryset = Source.objects.select_related(
"segment", "provenance", "holding_institution"
).order_by("siglum")
)

display_unpublished: bool = self.request.user.is_authenticated
if display_unpublished:
if self.request.user.is_authenticated:
q_obj_filter = Q()
else:
q_obj_filter = Q(published=True)

if self.request.GET.get("century"):
century_name = Century.objects.get(id=self.request.GET.get("century")).name
if century_id := self.request.GET.get("century"):
century_name = Century.objects.get(id=century_id).name
q_obj_filter &= Q(century__name__icontains=century_name)

if self.request.GET.get("provenance"):
provenance_id = int(self.request.GET.get("provenance"))
q_obj_filter &= Q(provenance__id=provenance_id)
if self.request.GET.get("segment"):
segment_id = int(self.request.GET.get("segment"))
q_obj_filter &= Q(segment__id=segment_id)
if self.request.GET.get("fullSource") in ["true", "false"]:
full_source_str = self.request.GET.get("fullSource")
if provenance_id := self.request.GET.get("provenance"):
q_obj_filter &= Q(provenance__id=int(provenance_id))
if segment_id := self.request.GET.get("segment"):
q_obj_filter &= Q(segment__id=int(segment_id))
if (full_source_str := self.request.GET.get("fullSource")) in ["true", "false"]:
if full_source_str == "true":
full_source_q = Q(full_source=True) | Q(full_source=None)
q_obj_filter &= full_source_q
else:
q_obj_filter &= Q(full_source=False)

if self.request.GET.get("general"):
if general_str := self.request.GET.get("general"):
# Strip spaces at the beginning and end. Then make list of terms split on spaces
general_search_terms = self.request.GET.get("general").strip(" ").split(" ")
general_search_terms = general_str.strip(" ").split(" ")
# We need a Q Object for each field we're gonna look into
shelfmark_q = Q()
siglum_q = Q()
holding_institution_q = Q()
holding_institution_city_q = Q()
description_q = Q()
# it seems that old cantus don't look into title and provenance for the general search terms
# cantus.uwaterloo.ca/source/123901 this source cannot be found by searching its provenance 'Kremsmünster' in the general search field
# it seems that old cantus don't look into title and provenance
# for the general search terms
# cantus.uwaterloo.ca/source/123901 this source cannot be found by searching
# its provenance 'Kremsmünster' in the general search field
# provenance_q = Q()
summary_q = Q()

Expand Down Expand Up @@ -300,9 +300,9 @@ def get_queryset(self):

# For the indexing notes search we follow the same procedure as above but with
# different fields
if self.request.GET.get("indexing"):
if indexing_str := self.request.GET.get("indexing"):
# Make list of terms split on spaces
indexing_search_terms = self.request.GET.get("indexing").split(" ")
indexing_search_terms = indexing_str.strip(" ").split(" ")
# We need a Q Object for each field we're gonna look into
inventoried_by_q = Q()
full_text_entered_by_q = Q()
Expand Down Expand Up @@ -338,8 +338,23 @@ def get_queryset(self):
)
q_obj_filter &= indexing_search_q

order_param = self.request.GET.get("order")
order_fields = ["siglum"]
if order_param == "country":
order_fields.insert(0, "holding_institution__country")
if order_param == "heading":
order_fields.insert(0, "holding_institution__city")
order_fields.insert(1, "holding_institution__name")
if self.request.GET.get("sort") == "desc":
sort_prefix = "-"
else:
sort_prefix = ""

order_by_args = [f"{sort_prefix}{field}" for field in order_fields]

return (
queryset.filter(q_obj_filter)
.order_by(*order_by_args)
.distinct()
.prefetch_related(
Prefetch("century", queryset=Century.objects.all().order_by("id"))
Expand Down
Loading