"""Class to divide large lists of direcrories and files into pages."""
from __future__ import annotations
from json import loads
from re import sub as re_sub, Match
from pyramid.request import Request
from chrysalio.lib.paging import DISPLAYS as PAGING_DISPLAYS
from chrysalio.lib.paging import PAGE_SIZES as PAGING_PAGE_SIZES
from chrysalio.lib.paging import PAGE_DEFAULT_SIZE
from chrysalio.lib.log import log_error
from chrysalio.helpers import tags
from chrysalio.helpers.literal import Literal
from .ciopath import CioPath
from .ciotype import CioType
from .i18n import _
DISPLAYS = ('snippets', ) + PAGING_DISPLAYS
DISPLAY_LABELS = {
'snippets': _('Display with snippets'),
'cards': _('Display as cards'),
'list': _('Display as list')
}
DISPLAYED_LABELS = {
'snippets': _('Displayed with snippets'),
'cards': _('Displayed as cards'),
'list': _('Displayed as list')
}
PAGE_SIZES = PAGING_PAGE_SIZES[:-1]
# =============================================================================
[docs]
def sortable_column(
request: Request,
label: str,
sort: str,
current_sorting: str | None = None,
sortable: bool = True) -> str:
"""Output a header of column with `sort up` and `sort down` buttons.
:type request: pyramid.request.Request
:param request:
Current request.
:param str label:
Label of column.
:param str sort:
Sort criteria.
:param str current_sorting: (optional)
Default current sorting.
:param bool sortable: (default=True)
Non sortable field management.
:rtype: helpers.literal.Literal
"""
# Non sortable field
if not sortable:
return Literal(f'<span class="cioSortNone">{label}</span>')
# Sortable field
current = request.params.get('sort') or current_sorting
query_string = {}
if request.GET:
query_string.update(request.GET)
html = '<a title="{0}"'.format(
request.localizer.translate(_('Sort by ${l}', {'l': label.lower()})))
direction = current[0] if current else '+'
if current and sort == current[1:]:
html += ' class="cioSortAsc' \
if direction in ('+', '~') else ' class="cioSortDesc'
if direction == '~':
html += 'Only'
html += '"'
query_string['sort'] = f'-{sort}' \
if current and sort == current[1:] and direction == '+' \
else f'~{sort}' if direction == '~' else f'+{sort}'
html += ' href="{0}"'.format(
request.current_route_path(_query=query_string))
html += f'>{label}</a>'
return Literal(html)
# =============================================================================
[docs]
class FilePaging():
"""Divide large lists of direcrories and files into pages using a backend.
:type request: pyramid.request.Request
:param request:
Current request.
:type ciopath: .lib.ciopath.CioPath
:param ciopath:
CioPath of the current directory, possibly an empty one.
:param str query:
Tantivy query.
:param dict params: (optional)
Paging parameters: page number, page size, display mode and sort.
:param bool keep_list:
if ``True``, save a list of ``(ciopath, cioptype)`` in the user
session.
:param int snippets: (default = 0)
Length of the context to show.
"""
# -------------------------------------------------------------------------
def __init__(
self,
request: Request,
ciopath: CioPath,
query: str,
params: dict | None = None,
keep_list: bool = False,
snippets: int = 0):
"""Constructor method."""
# pylint: disable = too-many-arguments, too-many-positional-arguments
# pylint: disable = too-many-locals
# Initialize variables
self._request = request
self._backend = request.registry['modules']['ciowarehouse2'].backend
if params is None:
params = self.params(request, ciopath)
self.page_size = params['page_size']
self.display = params['display']
self.sort = params['sort']
# Retrieve the file list
offset = (params['page'] - 1) * self.page_size
sort_by = self.sort[1:] \
if self.sort and self.sort[1:] != 'score' else None
reverse = self.sort and self.sort[0] == '-'
reply, error = self._backend.search(
query,
limit=self.page_size,
offset=offset,
sort_by=sort_by,
reverse=reverse,
snippets=snippets)
if reply is None or error is not None:
self.items: tuple = ()
self.total = 0
self.page = 1
self.page_total = 1
log_error(request, error)
return
if reply.total < offset + 1:
sort_by = params['sort'][1:] \
if params['sort'] and params['sort'][1:] != 'score' else None
reverse = params['sort'] and params['sort'][0] == '-'
reply = self._backend.search(
query,
limit=self.page_size,
offset=0,
sort_by=sort_by,
reverse=reverse,
snippets=snippets)[0]
request.session['filepaging'][ciopath.wid]['pages'][
ciopath.path] = 1
# Fill the paging
items = []
kept = []
snippets_by_ciopath = {}
for snippet in reply.snippets:
snippets_by_ciopath[snippet.ciopath] = snippet.json
for hit in reply.hits:
values = loads(hit.json)
if snippets and values['ciopath'][0] in snippets_by_ciopath:
values['snippets'] = loads(
snippets_by_ciopath[values['ciopath'][0]])
values['score'] = hit.score
values['ciopath'] = CioPath.from_str(values['ciopath'][0])
values['ciotype'] = CioType.from_str(values['ciotype'][0])
values['icon'] = values['icon'][0]
values['thumbnail_ext'] = values['thumbnail_ext'][0] \
if values.get('thumbnail_ext') else None
values['uid'] = values['ciopath'].uid()
values['file_name'] = (values['ciopath'].file_name(), )
values['shared'] = 'shared' in values and values['shared'][0]
values['restricted'] = 'restricted' in values \
and values['restricted'][0]
items.append(values)
kept.append((values['ciopath'], values['ciotype']))
self.items = tuple(items)
self.total = reply.total
self.page = (reply.offset // self.page_size) + 1
self.page_total = ((reply.total - 1) // self.page_size) + 1 \
if reply.total else 1
if keep_list:
request.session['current_files'] = tuple(kept)
# -------------------------------------------------------------------------
[docs]
@classmethod
def params(
cls,
request: Request,
ciopath: CioPath,
default_sort: str | None = None,
default_display: str = 'cards') -> dict:
"""Set paging parameters into the paging session return it.
:type request: pyramid.request.Request
:param request:
Current request.
:type ciopath: .lib.ciopath.CioPath
:param ciopath:
CioPath of the current directory, possibly an empty one.
:param str default_sort: (optional)
Default sorting (ex. '+file_name').
:param str default_display: (optional)
Default display (ex. 'cards').
:rtype: dict
"""
# Initialization
if 'paging' not in request.session:
request.session['paging'] = \
request.registry['settings']['page-size'] \
if 'settings' in request.registry else PAGE_DEFAULT_SIZE, {}
if 'filepaging' not in request.session:
request.session['filepaging'] = {}
if ciopath.wid not in request.session['filepaging']:
request.session['filepaging'][ciopath.wid] = {
'pages': {},
'page_size': request.session['paging'][0],
'display': default_display,
'sort': default_sort
}
if ciopath.path not in request.session['filepaging'][
ciopath.wid]['pages']:
request.session['filepaging'][ciopath.wid]['pages'][
ciopath.path] = 1
# Current parameters
params = {
'page':
request.session['filepaging'][ciopath.wid]['pages'][ciopath.path],
'page_size':
request.session['filepaging'][ciopath.wid]['page_size'],
'display':
request.session['filepaging'][ciopath.wid]['display'],
'sort':
request.session['filepaging'][ciopath.wid]['sort'],
}
# Update parameters
if 'page' in request.params and request.params['page'].isdigit():
params['page'] = max(1, int(request.params['page']))
request.session['filepaging'][ciopath.wid]['pages'][
ciopath.path] = params['page']
if 'page_size' in request.params \
and request.params['page_size'].strip():
params['page_size'] = int(request.params['page_size'])
request.session['filepaging'][ciopath.wid]['page_size'] = \
params['page_size']
if request.params.get('display'):
params['display'] = request.params['display'] \
if request.params['display'] in DISPLAYS else default_display
request.session['filepaging'][ciopath.wid]['display'] = \
params['display']
if request.params.get('sort'):
params['sort'] = request.params['sort'] or default_sort
request.session['filepaging'][ciopath.wid]['sort'] = \
params['sort']
return params
# -------------------------------------------------------------------------
[docs]
def get_pfile(self, ciopath: CioPath) -> dict | None:
"""Retrieve the first item whose `CioPath` matches.
:type ciopath: .lib.ciopath.CioPath
:param ciopath:
`CioPath` to search.
:rtype: dict
"""
try:
return next((k for k in self.items if k['ciopath'] == ciopath))
except (StopIteration, AttributeError, TypeError):
return None
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
[docs]
def display_modes(self, modes: tuple = PAGING_DISPLAYS) -> str:
"""Output buttons to switch between snippets, cards and list mode.
:param tuple modes:
Modes to display.
:rtype: str
"""
html = ''
translate = self._request.localizer.translate
qstring = self._request.GET.copy()
for mode in DISPLAYS:
if mode in modes:
if self.display == mode:
html = f'{html}'\
' <span class="cioPagingDisplay{0}"'\
' title="{1}"> </span> '.format(
mode.capitalize(),
translate(DISPLAYED_LABELS[mode]))
else:
qstring['display'] = mode
html = f'{html}'\
' <a href="{0}" class="cioPagingDisplay{1}"'\
' title="{2}"> </a> '.format(
self._request.current_route_path(_query=qstring),
mode.capitalize(),
translate(DISPLAY_LABELS[mode]))
return Literal(f'<span class="cioPagingDisplay">{html}</span>')
# -------------------------------------------------------------------------
[docs]
def sortable_column(
self, label: str, sort: str, sortable: bool = True) -> str:
"""Output a header of column with `sort up` and `sort down` buttons.
See :func:`sortable_column`.
:param str label:
Label of column.
:param str sort:
Sort criteria.
:param bool sortable: (default=True)
Non sortable field management.
:rtype: helpers.literal.Literal
"""
return sortable_column(self._request, label, sort, self.sort, sortable)
# -------------------------------------------------------------------------
def _range(self, regex_match: Match) -> str:
"""Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
:type regex_match: re.Match
:param regex_match:
A regular expression match object containing the radius of linked
pages around the current page in regex_match.group(1) as a string.
:rtype: str
"""
query_string = self._request.GET.copy()
radius = int(regex_match.group(1))
leftmost_page = max(1, self.page - radius)
rightmost_page = min(self.page + radius, self.page_total)
items = []
if self.page != 1 and leftmost_page > 1:
items.append(self._link(query_string, '1', 1))
if leftmost_page > 2:
items.append('…')
for page in range(leftmost_page, rightmost_page + 1):
if page == self.page:
items.append(f'<span>{page}</span>')
else:
items.append(self._link(query_string, str(page), page))
if rightmost_page < self.page_total - 1:
items.append('…')
if self.page != self.page_total and rightmost_page < self.page_total:
items.append(
self._link(
query_string, str(self.page_total), self.page_total))
return ' '.join(items)
# -------------------------------------------------------------------------
def _link(
self,
query_string: dict,
label: str,
page_number: int | None = None) -> str:
"""Create an A-HREF tag.
:param dict query_string:
The current query string in a dictionary.
:param str label:
Text to be printed in the A-HREF tag.
:param int page_number: (optional)
Number of the page that the link points to.
:rtype: str
"""
if page_number:
query_string.update({'page': page_number})
return tags.link_to(
label, self._request.current_route_path(_query=query_string))