Skip to content

Commit

Permalink
Merge pull request #1592 from dchiller/i1217-source-list-sortable-col…
Browse files Browse the repository at this point in the history
…umns

Make "Country" and "Source" columns sortable on Source List page
  • Loading branch information
dchiller committed Aug 13, 2024
2 parents fb455c6 + a206538 commit ee12c96
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 27 deletions.
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

0 comments on commit ee12c96

Please sign in to comment.