feat: use Query for children instead of Record list
This commit is contained in:
@@ -104,7 +104,7 @@ The built-in field attributes are:
|
|||||||
- `key`: slugified group value, e.g., "a-title"
|
- `key`: slugified group value, e.g., "a-title"
|
||||||
- `slug`: url path after root node, e.g. "config/a-title.html" (can be `None`)
|
- `slug`: url path after root node, e.g. "config/a-title.html" (can be `None`)
|
||||||
- `record`: parent node, e.g., `Page(path="/")`
|
- `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)
|
- `config`: configuration object (see below)
|
||||||
|
|
||||||
Without any changes, the `key` value will just be `slugify(group)`.
|
Without any changes, the `key` value will just be `slugify(group)`.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<h2>Path: {{ this | url(absolute=True) }}</h2>
|
<h2>Path: {{ this | url(absolute=True) }}</h2>
|
||||||
<p>This is: {{this}}</p>
|
<p>This is: {{this}}</p>
|
||||||
<p>Custom field, desc: "{{this.desc}}"</p>
|
<p>Custom field, desc: "{{this.desc}}"</p>
|
||||||
<p>Children: {{this.children}}</p>
|
<p>Children: {{this.children.all()}}</p>
|
||||||
|
|||||||
72
lektor_groupby/query.py
Normal file
72
lektor_groupby/query.py
Normal file
@@ -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]
|
||||||
@@ -4,7 +4,9 @@ from lektor.db import _CmpHelper
|
|||||||
from lektor.environment import Expression
|
from lektor.environment import Expression
|
||||||
from lektor.sourceobj import VirtualSourceObject # subclass
|
from lektor.sourceobj import VirtualSourceObject # subclass
|
||||||
from lektor.utils import build_url
|
from lektor.utils import build_url
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, List, Any, Optional, Iterator, Iterable
|
from typing import TYPE_CHECKING, List, Any, Optional, Iterator, Iterable
|
||||||
|
from .query import FixedRecordsQuery
|
||||||
from .util import report_config_error, most_used_key
|
from .util import report_config_error, most_used_key
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from lektor.builder import Artifact
|
from lektor.builder import Artifact
|
||||||
@@ -28,14 +30,14 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
def __init__(self, record: 'Record', slug: str) -> None:
|
def __init__(self, record: 'Record', slug: str) -> None:
|
||||||
super().__init__(record)
|
super().__init__(record)
|
||||||
self.key = slug
|
self.key = slug
|
||||||
self._group_map = [] # type: List[str]
|
self.__children = [] # type: List[str]
|
||||||
self._children = [] # type: List[Record]
|
self.__group_map = [] # type: List[str]
|
||||||
|
|
||||||
def append_child(self, child: 'Record', group: str) -> None:
|
def append_child(self, child: 'Record', group: str) -> None:
|
||||||
if child not in self._children:
|
if child not in self.__children:
|
||||||
self._children.append(child)
|
self.__children.append(child.path)
|
||||||
# _group_map is later used to find most used group
|
# __group_map is later used to find most used group
|
||||||
self._group_map.append(group)
|
self.__group_map.append(group)
|
||||||
|
|
||||||
# -------------------------
|
# -------------------------
|
||||||
# Evaluate Extra Fields
|
# Evaluate Extra Fields
|
||||||
@@ -44,8 +46,14 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
def finalize(self, config: 'Config', group: Optional[str] = None) \
|
def finalize(self, config: 'Config', group: Optional[str] = None) \
|
||||||
-> 'GroupBySource':
|
-> 'GroupBySource':
|
||||||
self.config = config
|
self.config = config
|
||||||
self.group = group or most_used_key(self._group_map)
|
# make a sorted children query
|
||||||
del self._group_map
|
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
|
# evaluate slug Expression
|
||||||
if config.slug and '{key}' in config.slug:
|
if config.slug and '{key}' in config.slug:
|
||||||
self.slug = config.slug.replace('{key}', self.key)
|
self.slug = config.slug.replace('{key}', self.key)
|
||||||
@@ -57,10 +65,6 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
# extra fields
|
# extra fields
|
||||||
for attr, expr in config.fields.items():
|
for attr, expr in config.fields.items():
|
||||||
setattr(self, attr, self._eval(expr, field='fields.' + attr))
|
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
|
return self
|
||||||
|
|
||||||
def _eval(self, value: Any, *, field: str) -> Any:
|
def _eval(self, value: Any, *, field: str) -> Any:
|
||||||
@@ -93,7 +97,7 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
''' Enumerate all dependencies '''
|
''' Enumerate all dependencies '''
|
||||||
if self.config.dependencies:
|
if self.config.dependencies:
|
||||||
yield from self.config.dependencies
|
yield from self.config.dependencies
|
||||||
for record in self._children:
|
for record in self.children:
|
||||||
yield from record.iter_source_filenames()
|
yield from record.iter_source_filenames()
|
||||||
|
|
||||||
# def get_checksum(self, path_cache: 'PathCache') -> Optional[str]:
|
# def get_checksum(self, path_cache: 'PathCache') -> Optional[str]:
|
||||||
@@ -101,20 +105,9 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
# self.config.template).filename]
|
# self.config.template).filename]
|
||||||
# deps.extend(self.iter_source_filenames())
|
# deps.extend(self.iter_source_filenames())
|
||||||
# sums = '|'.join(path_cache.get_file_info(x).filename_and_checksum
|
# 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
|
# 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 get_sort_key(self, fields: Iterable[str]) -> List:
|
||||||
def cmp_val(field: str) -> Any:
|
def cmp_val(field: str) -> Any:
|
||||||
reverse = field.startswith('-')
|
reverse = field.startswith('-')
|
||||||
@@ -129,17 +122,10 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
# -----------------------
|
# -----------------------
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children(self) -> List['Record']:
|
def children(self) -> FixedRecordsQuery:
|
||||||
''' Returns dict with page record key and (optional) extra value. '''
|
''' Return query of children of type Record. '''
|
||||||
return self._children
|
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:
|
def __getitem__(self, key: str) -> Any:
|
||||||
# Used for virtual path resolver
|
# Used for virtual path resolver
|
||||||
if key in ('_path', '_alt'):
|
if key in ('_path', '_alt'):
|
||||||
@@ -163,7 +149,7 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return '<GroupBySource path="{}" children={}>'.format(
|
return '<GroupBySource path="{}" children={}>'.format(
|
||||||
self.path, len(self._children))
|
self.path, self.children.count())
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------
|
# -----------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user