Skip to content

Commit

Permalink
Merge pull request #1057 from ecds/feature/956-single-vol-search
Browse files Browse the repository at this point in the history
Add UI components: collection grid and list views, static pages, single collection banner and modal, and minor aesthetic tweaks
  • Loading branch information
yl5682 authored Sep 23, 2024
2 parents 2624d86 + a18808a commit b1151c2
Show file tree
Hide file tree
Showing 40 changed files with 1,110 additions and 1,178 deletions.
208 changes: 135 additions & 73 deletions apps/cms/models.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
"""CMS Models."""
from urllib.parse import urlencode
from modelcluster.fields import ParentalManyToManyField
from wagtail.models import Page
from wagtail.fields import RichTextField, StreamField
from wagtail.admin.panels import FieldPanel
from wagtailautocomplete.edit_handlers import AutocompletePanel
from django.db import models
from apps.cms.blocks import BaseStreamBlock
from apps.readux.forms import AllCollectionsForm, AllVolumesForm
from apps.readux.models import UserAnnotation
from apps.iiif.kollections.models import Collection
from apps.iiif.manifests.models import Manifest
from apps.iiif.canvases.models import Canvas
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator


Expand Down Expand Up @@ -39,14 +38,87 @@ class CollectionsPage(Page):
default="List",
help_text="Select to show all volumes as a list or a grid of icons."
)
collections = Collection.objects.all
volumes = Manifest.objects.all
collections = Collection.objects.all()
volumes = Manifest.objects.all()
content_panels = Page.content_panels + [
FieldPanel('page_title', classname="full"),
FieldPanel('tagline', classname="full"),
FieldPanel('paragraph', classname="full"),
FieldPanel('layout', classname="full"),
]
initial = {"sort": "title", "order": "asc", "display": "grid"}
sort_fields = {
"title": "label",
"added": "created_at",
"volumes": "volumes_count",
}

def get_form(self, request):
"""Get the form for setting sort and order"""
# use GET instead of default POST/PUT for form data
form_data = request.GET.copy()

# sort by chosen sort
if "sort" in form_data and bool(form_data.get("sort")):
form_data["sort"] = form_data.get("sort")

# Otherwise set all form values to default
for key, val in self.initial.items():
form_data.setdefault(key, val)

return AllCollectionsForm(data=form_data)

def get_queryset(self, form, queryset):
"""Get the sorted set of objects to display"""

# return empty queryset if not valid
if not form.is_valid():
return queryset.none()

# get sort and order selections from form
search_opts = form.cleaned_data
sort = search_opts.get("sort", "title")
if sort not in self.sort_fields:
sort = "title"
order = search_opts.get("order", "asc")
sign = "-" if order == "desc" else ""

# include volume count in queryset if sort
if "volumes" in sort:
queryset = queryset.annotate(volumes_count=models.Count("manifests"))

# build order_by query to sort results
queryset = queryset.order_by(f"{sign}{self.sort_fields[sort]}")

return queryset

def get_context(self, request):
"""Context function."""
context = super().get_context(request)

form = self.get_form(request)
query_set = self.get_queryset(form, self.collections)

paginator = Paginator(query_set, 8) # Show 8 collections per page

page = request.GET.get("page", 1)
try:
collections = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
collections = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
page = paginator.num_pages
collections = paginator.page(page)

context.update({
"form": form,
"collections": collections,
"paginator_range": paginator.get_elided_page_range(page, on_each_side=2),
})

return context

class VolumesPage(Page):
"""Content page"""
Expand All @@ -70,91 +142,81 @@ class VolumesPage(Page):
FieldPanel('paragraph', classname="full"),
FieldPanel('layout', classname="full"),
]
initial = {"sort": "title", "order": "asc", "display": "grid"}
sort_fields = {
"title": "label",
"author": "author",
"date": "date_sort_ascending",
"added": "created_at",
}

def get_form(self, request):
"""Get the form for setting sort and order"""
# use GET instead of default POST/PUT for form data
form_data = request.GET.copy()

# sort by chosen sort
if "sort" in form_data and bool(form_data.get("sort")):
form_data["sort"] = form_data.get("sort")

# Otherwise set all form values to default
for key, val in self.initial.items():
form_data.setdefault(key, val)

return AllVolumesForm(data=form_data)

def get_queryset(self, form, queryset):
"""Get the sorted set of objects to display"""

# return empty queryset if not valid
if not form.is_valid():
return queryset.none()

# get sort and order selections from form
search_opts = form.cleaned_data
sort = search_opts.get("sort", "title")
if sort not in self.sort_fields:
sort = "title"
order = search_opts.get("order", "asc")
sign = "-" if order == "desc" else ""

# build order_by query to sort results
if sort == "date" and order == "desc":
# special case for date, descending: need to use date_sort_descending field
# and sort nulls last
queryset = queryset.order_by(models.F("date_sort_descending").desc(nulls_last=True))
else:
queryset = queryset.order_by(f"{sign}{self.sort_fields[sort]}")

return queryset

def get_context(self, request):
"""Context function."""
context = super().get_context(request)
sort = request.GET.get('sort', None)
order = request.GET.get('order', None)
query_set = self.volumes

form = self.get_form(request)
query_set = self.get_queryset(form, self.volumes)

sort_options = ['title', 'author', 'date published', 'date added']
order_options = ['asc', 'desc']
if sort not in sort_options and order not in order_options:
sort = 'title'
order = 'asc'
elif sort not in sort_options:
sort = 'title'
elif order not in order_options:
order = 'asc'

if sort == 'title':
if order == 'asc':
query_set = query_set.order_by('label')
elif order == 'desc':
query_set = query_set.order_by('-label')
elif sort == 'author':
if order == 'asc':
query_set = query_set.order_by('author')
elif order == 'desc':
query_set = query_set.order_by('-author')
elif sort == 'date published':
if order == 'asc':
query_set = query_set.order_by('published_date_edtf')
elif order == 'desc':
query_set = query_set.order_by('-published_date_edtf')
elif sort == 'date added':
if order == 'asc':
query_set = query_set.order_by('created_at')
elif order == 'desc':
query_set = query_set.order_by('-created_at')

sort_url_params = request.GET.copy()
order_url_params = request.GET.copy()
if 'sort' in sort_url_params and 'order' in order_url_params:
del sort_url_params['sort']
del order_url_params['order']
elif 'sort' in sort_url_params:
del sort_url_params['sort']
elif 'order' in order_url_params:
del order_url_params['order']

paginator = Paginator(query_set, 10) # Show 10 volumes per page

page = request.GET.get('page')
paginator = Paginator(query_set, 8) # Show 8 volumes per page

page = request.GET.get("page", 1)
try:
volumes = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
volumes = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
volumes = paginator.page(paginator.num_pages)

# make the variable 'volumes' available on the template
context['volumes'] = volumes
# context['volumespage'] = query_set.all
context['user_annotation'] = UserAnnotation.objects.filter(owner_id=request.user.id)
# annocount_list = []
# canvaslist = []
# for volume in query_set:
# user_annotation_count = UserAnnotation.objects.filter(owner_id=request.user.id).filter(canvas__manifest__id=volume.id).count()
# annocount_list.append({volume.pid: user_annotation_count})
# context['user_annotation_count'] = annocount_list
# canvasquery = Canvas.objects.filter(is_starting_page=1).filter(manifest__id=volume.id)
# canvasquery2 = list(canvasquery)
# canvaslist.append({volume.pid: canvasquery2})
# context['firstthumbnail'] = canvaslist
# value = 0
# context['value'] = value
page = paginator.num_pages
volumes = paginator.page(page)

context.update({
'sort_url_params': urlencode(sort_url_params),
'order_url_params': urlencode(order_url_params),
'sort': sort, 'sort_options': sort_options,
'order': order, 'order_options': order_options,
"form": form,
"volumes": volumes,
"user_annotation": UserAnnotation.objects.filter(owner_id=request.user.id),
"paginator_range": paginator.get_elided_page_range(page, on_each_side=2),
})

return context


Expand Down
5 changes: 3 additions & 2 deletions apps/iiif/kollections/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from django.contrib import admin
from import_export import resources
from import_export.admin import ImportExportModelAdmin
from django_summernote.admin import SummernoteModelAdmin

from apps.iiif.manifests.admin import SummernoteMixin
from .models import Collection
from ..manifests.models import Manifest

Expand Down Expand Up @@ -37,7 +38,7 @@ def manifest_pid(self, instance):

manifest_pid.short_description = 'Manifest Local ID'

class CollectionAdmin(ImportExportModelAdmin, SummernoteModelAdmin, admin.ModelAdmin):
class CollectionAdmin(ImportExportModelAdmin, SummernoteMixin, admin.ModelAdmin):
"""Django admin configuration for a collection."""
inlines = [
ManifestInline,
Expand Down
13 changes: 12 additions & 1 deletion apps/iiif/manifests/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,18 @@ class RelatedLinksInline(admin.TabularInline):
extra = 1
min_num = 0

class ManifestAdmin(ImportExportModelAdmin, SummernoteModelAdmin, admin.ModelAdmin):
class SummernoteMixin(SummernoteModelAdmin):
class Media:
# NOTE: have to include these js and css dependencies for summernote when not using iframe
js = (
"//code.jquery.com/jquery-3.7.1.min.js",
"//cdn.jsdelivr.net/npm/[email protected]/jquery.ui.widget.min.js",
)
css = {
"all": ["//cdn.jsdelivr.net/npm/[email protected]/dist/summernote-lite.min.css"],
}

class ManifestAdmin(ImportExportModelAdmin, SummernoteMixin, admin.ModelAdmin):
"""Django admin configuration for manifests"""
resource_class = ManifestResource
exclude = ('id',)
Expand Down
67 changes: 67 additions & 0 deletions apps/readux/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dateutil import parser
from django import forms
from django.forms import widgets
from django.conf import settings
from django.template.defaultfilters import truncatechars

Expand Down Expand Up @@ -181,3 +182,69 @@ def set_date(self, min_date, max_date):
self.fields["start_date"].set_initial(min_date_object.strftime("%Y-%m-%d"))
max_date_object = parser.isoparse(max_date)
self.fields["end_date"].set_initial(max_date_object.strftime("%Y-%m-%d"))


class CustomDropdownSelect(widgets.ChoiceWidget):
"""A custom select widget that uses uk-navbar-dropdown to present its options,
and submits the form on any change"""
input_type = "radio"
template_name = "widgets/custom_dropdown_select.html"
option_template_name = "django/forms/widgets/radio_option.html"

def get_context(self, name, value, attrs):
"""Add the label for the selected option to template context"""
context = super().get_context(name, value, attrs)
context["selected_value_label"] = dict(self.choices).get(value, None)
return context


class AllVolumesForm(forms.Form):
"""Simple form for sorting Manifests"""

SORT_CHOICES = [
("title", "Title"),
("author", "Author"),
("date", "Publication Year"),
("added", "Date Added"),
]
ORDER_CHOICES = [
("asc", "Ascending"),
("desc", "Descending"),
]
sort = forms.ChoiceField(
label="Sort by",
choices=SORT_CHOICES,
required=False,
widget=CustomDropdownSelect,
)
order = forms.ChoiceField(
label="Order",
choices=ORDER_CHOICES,
required=False,
widget=CustomDropdownSelect,
)

class AllCollectionsForm(forms.Form):
"""Simple form for sorting Collections"""

SORT_CHOICES = [
("title", "Title"),
("added", "Date Added"),
("volumes", "Number of Volumes"),
]
ORDER_CHOICES = [
("asc", "Ascending"),
("desc", "Descending"),
]
sort = forms.ChoiceField(
label="Sort by",
choices=SORT_CHOICES,
required=False,
widget=CustomDropdownSelect,
)
order = forms.ChoiceField(
label="Order",
choices=ORDER_CHOICES,
required=False,
widget=CustomDropdownSelect,
)
20 changes: 20 additions & 0 deletions apps/readux/templates/widgets/custom_dropdown_select.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{# Adapted from django/forms/widgets/radio.html to use uk-navbar-dropdown #}

{% with id=widget.attrs.id %}
<a href="#">{{ selected_value_label|default_if_none:widget.optgroups.0.1.0.label }}</a>
<div class="uk-navbar-dropdown" uk-dropdown="mode: click">
<ul class="uk-nav uk-navbar-dropdown-nav sort-dropdown"{% if id %} id="{{ id }}"{% endif %}>
{% for group, options, index in widget.optgroups %}
{% for option in options %}
<li>
<label {% if option.attrs.id %} for="{{ option.attrs.id }}"{% endif %}>
{# NOTE: Uses a tiny bit of inline JS to submit the form on change #}
<input onclick="this.form.submit()" type="{{ option.type }}" name="{{ option.name }}" value="{{ option.value }}" {% include "django/forms/widgets/attrs.html" with widget=option %}>
{{ option.label }}
</label>
</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endwith %}
Loading

0 comments on commit b1151c2

Please sign in to comment.