diff --git a/examples/README.md b/examples/README.md index 742611a..7d3f270 100644 --- a/examples/README.md +++ b/examples/README.md @@ -104,7 +104,7 @@ The built-in field attributes are: - `key`: slugified group value, e.g., "a-title" - `slug`: url path after root node, e.g. "config/a-title.html" (can be `None`) - `record`: parent node, e.g., `Page(path="/")` -- `children`: the elements of the grouping (`Record` type) +- `children`: the elements of the grouping (a `Query` of `Record` type) - `config`: configuration object (see below) Without any changes, the `key` value will just be `slugify(group)`. diff --git a/examples/templates/example-advanced.html b/examples/templates/example-advanced.html index 6cdbb3f..458c81d 100644 --- a/examples/templates/example-advanced.html +++ b/examples/templates/example-advanced.html @@ -1,4 +1,4 @@

Path: {{ this | url(absolute=True) }}

This is: {{this}}

Custom field, desc: "{{this.desc}}"

-

Children: {{this.children}}

+

Children: {{this.children.all()}}

diff --git a/lektor_groupby/query.py b/lektor_groupby/query.py new file mode 100644 index 0000000..eb7312c --- /dev/null +++ b/lektor_groupby/query.py @@ -0,0 +1,72 @@ +# adapting https://github.com/dairiki/lektorlib/blob/master/lektorlib/query.py +from lektor.constants import PRIMARY_ALT +from lektor.db import Query # subclass + +from typing import TYPE_CHECKING, List, Optional, Iterator, Iterable +if TYPE_CHECKING: + from lektor.db import Record, Pad + + +class FixedRecordsQuery(Query): + def __init__( + self, pad: 'Pad', child_paths: Iterable[str], alt: str = PRIMARY_ALT + ): + ''' Query with a pre-defined list of children of type Record. ''' + super().__init__('/', pad, alt=alt) + self.__child_paths = [x.lstrip('/') for x in child_paths] + + def _get( + self, path: str, persist: bool = True, page_num: Optional[int] = None + ) -> Optional['Record']: + ''' Internal getter for a single Record. ''' + if path not in self.__child_paths: + return None + if page_num is None: + page_num = self._page_num + return self.pad.get( # type: ignore[no-any-return] + path, alt=self.alt, page_num=page_num, persist=persist) + + def _iterate(self) -> Iterator['Record']: + ''' Iterate over internal set of Record elements. ''' + # ignore self record dependency from super() + for path in self.__child_paths: + record = self._get(path, persist=False) + if record is None: + if self._page_num is not None: + # Sanity check: ensure the unpaginated version exists + unpaginated = self._get(path, persist=False, page_num=None) + if unpaginated is not None: + # Requested explicit page_num, but source does not + # support pagination. Punt and skip it. + continue + raise RuntimeError('could not load source for ' + path) + + is_attachment = getattr(record, 'is_attachment', False) + if self._include_attachments and not is_attachment \ + or self._include_pages and is_attachment: + continue + if self._matches(record): + yield record + + def get_order_by(self) -> Optional[List[str]]: + ''' Return list of attribute strings for sort order. ''' + # ignore datamodel ordering from super() + return self._order_by + + def count(self) -> int: + ''' Count matched objects. ''' + if self._pristine: + return len(self.__child_paths) + return super().count() # type: ignore[no-any-return] + + def get(self, path: str, page_num: Optional[int] = None) \ + -> Optional['Record']: + ''' Return Record with given path ''' + if path in self.__child_paths: + return self._get(path, page_num=page_num) + return None + + def __bool__(self) -> bool: + if self._pristine: + return len(self.__child_paths) > 0 + return super().__bool__() # type: ignore[no-any-return] diff --git a/lektor_groupby/vobj.py b/lektor_groupby/vobj.py index 7cd43bc..a3c9db5 100644 --- a/lektor_groupby/vobj.py +++ b/lektor_groupby/vobj.py @@ -4,7 +4,9 @@ from lektor.db import _CmpHelper from lektor.environment import Expression from lektor.sourceobj import VirtualSourceObject # subclass from lektor.utils import build_url + from typing import TYPE_CHECKING, List, Any, Optional, Iterator, Iterable +from .query import FixedRecordsQuery from .util import report_config_error, most_used_key if TYPE_CHECKING: from lektor.builder import Artifact @@ -28,14 +30,14 @@ class GroupBySource(VirtualSourceObject): def __init__(self, record: 'Record', slug: str) -> None: super().__init__(record) self.key = slug - self._group_map = [] # type: List[str] - self._children = [] # type: List[Record] + self.__children = [] # type: List[str] + self.__group_map = [] # type: List[str] def append_child(self, child: 'Record', group: str) -> None: - if child not in self._children: - self._children.append(child) - # _group_map is later used to find most used group - self._group_map.append(group) + if child not in self.__children: + self.__children.append(child.path) + # __group_map is later used to find most used group + self.__group_map.append(group) # ------------------------- # Evaluate Extra Fields @@ -44,8 +46,14 @@ class GroupBySource(VirtualSourceObject): def finalize(self, config: 'Config', group: Optional[str] = None) \ -> 'GroupBySource': self.config = config - self.group = group or most_used_key(self._group_map) - del self._group_map + # make a sorted children query + self._children = FixedRecordsQuery(self.pad, self.__children, self.alt) + self._children._order_by = config.order_by + # set group name + self.group = group or most_used_key(self.__group_map) + # cleanup temporary data + del self.__children + del self.__group_map # evaluate slug Expression if config.slug and '{key}' in config.slug: self.slug = config.slug.replace('{key}', self.key) @@ -57,10 +65,6 @@ class GroupBySource(VirtualSourceObject): # extra fields for attr, expr in config.fields.items(): setattr(self, attr, self._eval(expr, field='fields.' + attr)) - # sort children - if config.order_by: - # using get_sort_key() of Record - self._children.sort(key=lambda x: x.get_sort_key(config.order_by)) return self def _eval(self, value: Any, *, field: str) -> Any: @@ -93,7 +97,7 @@ class GroupBySource(VirtualSourceObject): ''' Enumerate all dependencies ''' if self.config.dependencies: yield from self.config.dependencies - for record in self._children: + for record in self.children: yield from record.iter_source_filenames() # def get_checksum(self, path_cache: 'PathCache') -> Optional[str]: @@ -101,20 +105,9 @@ class GroupBySource(VirtualSourceObject): # self.config.template).filename] # deps.extend(self.iter_source_filenames()) # sums = '|'.join(path_cache.get_file_info(x).filename_and_checksum - # for x in deps if x) + str(len(self._children)) + # for x in deps if x) + str(self.children.count()) # return hashlib.sha1(sums.encode('utf-8')).hexdigest() if sums else None - # @property - # def pagination(self): - # print('pagination') - # return None - - # def __for_page__(self, page_num): - # """Get source object for a (possibly) different page number. - # """ - # print('for page', page_num) - # return self - def get_sort_key(self, fields: Iterable[str]) -> List: def cmp_val(field: str) -> Any: reverse = field.startswith('-') @@ -129,17 +122,10 @@ class GroupBySource(VirtualSourceObject): # ----------------------- @property - def children(self) -> List['Record']: - ''' Returns dict with page record key and (optional) extra value. ''' + def children(self) -> FixedRecordsQuery: + ''' Return query of children of type Record. ''' return self._children - @property - def first_child(self) -> Optional['Record']: - ''' Returns first referencing page record. ''' - if self._children: - return iter(self._children).__next__() - return None - def __getitem__(self, key: str) -> Any: # Used for virtual path resolver if key in ('_path', '_alt'): @@ -163,7 +149,7 @@ class GroupBySource(VirtualSourceObject): def __repr__(self) -> str: return ''.format( - self.path, len(self._children)) + self.path, self.children.count()) # -----------------------------------