Source code for ciowarehouse2.lib.file_paging

"""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 pager_top(self) -> str: """Output a string with links to first, previous, next and last pages. :rtye: str """ html = '' translate = self._request.localizer.translate qstring = self._request.GET.copy() first_item = min((self.page - 1) * self.page_size + 1, self.total) last_item = min(first_item + self.page_size - 1, self.total) \ if self.page_size else self.total # First & previous if self.page > 1: qstring['page'] = 1 html = f'{html}'\ '<a href="{0}" class="cioPageFirst" title="{1}"> </a> '.format( self._request.current_route_path(_query=qstring), translate(_('First page'))) qstring['page'] = self.page - 1 html = f'{html}'\ '<a href="{0}" class="cioPagePrevious" title="{1}"> </a>'\ .format( self._request.current_route_path(_query=qstring), translate(_('Previous page'))) else: html = f'{html}'\ '<span class="cioPageFirst" title="{0}"> </span> '\ '<span class="cioPagePrevious" title="{1}"> </span>'.format( translate(_('First page')), translate(_('Previous page'))) # Items html = f'{html} {first_item}{last_item} / {self.total} ' # Next & last if self.page < self.page_total: qstring['page'] = self.page + 1 html = f'{html}'\ '<a href="{0}" class="cioPageNext" title="{1}"> </a> '.format( self._request.current_route_path(_query=qstring), translate(_('Next page'))) qstring['page'] = self.page_total html = f'{html}'\ '<a href="{0}" class="cioPageLast" title="{1}"> </a>'.format( self._request.current_route_path(_query=qstring), translate(_('Last page'))) else: html = f'{html}'\ '<span class="cioPageNext" title="{0}"> </span> '\ '<span class="cioPageLast" title="{1}"> </span>'.format( translate(_('Next page')), translate(_('Last page'))) return Literal(f'<span class="cioPagingPages">{html}</span>')
# -------------------------------------------------------------------------
[docs] def pager_bottom(self, pager_format: str = '~4~') -> str: """Output a string with links to some previous and next pages. :param str pager_format: (default='~4~') Format string that defines how the pager is rendered. :rtype: str """ # Replace ~...~ in token format by range of pages return Literal(re_sub(r'~(\d+)~', self._range, pager_format))
# -------------------------------------------------------------------------
[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))