feat: add order_by to group children
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
from lektor.context import get_ctx
|
from lektor.context import get_ctx
|
||||||
from typing import TYPE_CHECKING, Union, Iterable, Iterator, Optional
|
from typing import TYPE_CHECKING, Union, Iterable, Iterator, Optional
|
||||||
import weakref
|
import weakref
|
||||||
|
from .util import split_strip
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from lektor.builder import Builder
|
from lektor.builder import Builder
|
||||||
from lektor.db import Record
|
from lektor.db import Record
|
||||||
@@ -48,7 +49,7 @@ class VGroups:
|
|||||||
fields: Union[str, Iterable[str], None] = None,
|
fields: Union[str, Iterable[str], None] = None,
|
||||||
flows: Union[str, Iterable[str], None] = None,
|
flows: Union[str, Iterable[str], None] = None,
|
||||||
recursive: bool = False,
|
recursive: bool = False,
|
||||||
order_by: Optional[str] = None,
|
order_by: Union[str, Iterable[str], None] = None,
|
||||||
) -> Iterator['GroupBySource']:
|
) -> Iterator['GroupBySource']:
|
||||||
''' Extract all referencing groupby virtual objects from a page. '''
|
''' Extract all referencing groupby virtual objects from a page. '''
|
||||||
ctx = get_ctx()
|
ctx = get_ctx()
|
||||||
@@ -85,7 +86,13 @@ class VGroups:
|
|||||||
done_list.add(vobj())
|
done_list.add(vobj())
|
||||||
|
|
||||||
if order_by:
|
if order_by:
|
||||||
order = order_by.split(',')
|
if isinstance(order_by, str):
|
||||||
|
order = split_strip(order_by, ',')
|
||||||
|
elif isinstance(order_by, (list, tuple)):
|
||||||
|
order = order_by
|
||||||
|
else:
|
||||||
|
raise TypeError('order_by must be either str or list type.')
|
||||||
|
# using get_sort_key() of GroupBySource
|
||||||
yield from sorted(done_list, key=lambda x: x.get_sort_key(order))
|
yield from sorted(done_list, key=lambda x: x.get_sort_key(order))
|
||||||
else:
|
else:
|
||||||
yield from done_list
|
yield from done_list
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from inifile import IniFile
|
from inifile import IniFile
|
||||||
from lektor.utils import slugify
|
from lektor.utils import slugify
|
||||||
|
from .util import split_strip
|
||||||
|
|
||||||
from typing import Set, Dict, Optional, Union, Any
|
from typing import Set, Dict, Optional, Union, Any, List
|
||||||
|
|
||||||
AnyConfig = Union['Config', IniFile, Dict]
|
AnyConfig = Union['Config', IniFile, Dict]
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ class Config:
|
|||||||
self.dependencies = set() # type: Set[str]
|
self.dependencies = set() # type: Set[str]
|
||||||
self.fields = {} # type: Dict[str, Any]
|
self.fields = {} # type: Dict[str, Any]
|
||||||
self.key_map = {} # type: Dict[str, str]
|
self.key_map = {} # type: Dict[str, str]
|
||||||
|
self.order_by = None # type: Optional[List[str]]
|
||||||
|
|
||||||
def slugify(self, k: str) -> str:
|
def slugify(self, k: str) -> str:
|
||||||
''' key_map replace and slugify. '''
|
''' key_map replace and slugify. '''
|
||||||
@@ -48,6 +50,10 @@ class Config:
|
|||||||
''' This mapping replaces group keys before slugify. '''
|
''' This mapping replaces group keys before slugify. '''
|
||||||
self.key_map = key_map or {}
|
self.key_map = key_map or {}
|
||||||
|
|
||||||
|
def set_order_by(self, order_by: Optional[str]) -> None:
|
||||||
|
''' If specified, children will be sorted according to keys. '''
|
||||||
|
self.order_by = split_strip(order_by, ',') or None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
txt = '<GroupByConfig'
|
txt = '<GroupByConfig'
|
||||||
for x in ['key', 'root', 'slug', 'template', 'enabled']:
|
for x in ['key', 'root', 'slug', 'template', 'enabled']:
|
||||||
@@ -74,6 +80,7 @@ class Config:
|
|||||||
conf.dependencies.add(ini.filename)
|
conf.dependencies.add(ini.filename)
|
||||||
conf.set_fields(ini.section_as_dict(key + '.fields'))
|
conf.set_fields(ini.section_as_dict(key + '.fields'))
|
||||||
conf.set_key_map(ini.section_as_dict(key + '.key_map'))
|
conf.set_key_map(ini.section_as_dict(key + '.key_map'))
|
||||||
|
conf.set_order_by(ini.get(key + '.children.order_by', None))
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ def report_config_error(key: str, field: str, val: str, e: Exception) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def most_used_key(keys: List[str]) -> str:
|
def most_used_key(keys: List[str]) -> str:
|
||||||
|
''' Find string with most occurrences. '''
|
||||||
if len(keys) < 3:
|
if len(keys) < 3:
|
||||||
return keys[0] # TODO: first vs last occurrence
|
return keys[0] # TODO: first vs last occurrence
|
||||||
best_count = 0
|
best_count = 0
|
||||||
@@ -26,3 +27,13 @@ def most_used_key(keys: List[str]) -> str:
|
|||||||
best_count = num
|
best_count = num
|
||||||
best_key = k
|
best_key = k
|
||||||
return best_key
|
return best_key
|
||||||
|
|
||||||
|
|
||||||
|
def split_strip(data: str, delimiter: str = ',') -> List[str]:
|
||||||
|
''' Split by delimiter and strip each str separately. Omit if empty. '''
|
||||||
|
ret = []
|
||||||
|
for x in (data or '').split(delimiter):
|
||||||
|
x = x.strip()
|
||||||
|
if x:
|
||||||
|
ret.append(x)
|
||||||
|
return ret
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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, Dict, List, Any, Optional, Iterator, Iterable
|
from typing import TYPE_CHECKING, List, Any, Optional, Iterator, Iterable
|
||||||
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
|
||||||
@@ -29,13 +29,11 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
super().__init__(record)
|
super().__init__(record)
|
||||||
self.key = slug
|
self.key = slug
|
||||||
self._group_map = [] # type: List[str]
|
self._group_map = [] # type: List[str]
|
||||||
self._children = {} # type: Dict[Record, List[Any]]
|
self._children = [] # type: List[Record]
|
||||||
|
|
||||||
def append_child(self, child: 'Record', extra: Any, 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[child] = [extra]
|
self._children.append(child)
|
||||||
else:
|
|
||||||
self._children[child].append(extra)
|
|
||||||
# _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)
|
||||||
|
|
||||||
@@ -59,6 +57,10 @@ 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:
|
||||||
@@ -94,6 +96,25 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
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]:
|
||||||
|
# deps = [self.pad.env.jinja_env.get_or_select_template(
|
||||||
|
# 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))
|
||||||
|
# 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('-')
|
||||||
@@ -101,14 +122,14 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
field = field[1:]
|
field = field[1:]
|
||||||
return _CmpHelper(getattr(self, field, None), reverse)
|
return _CmpHelper(getattr(self, field, None), reverse)
|
||||||
|
|
||||||
return [cmp_val(field) for field in fields]
|
return [cmp_val(field) for field in fields or []]
|
||||||
|
|
||||||
# -----------------------
|
# -----------------------
|
||||||
# Properties & Helper
|
# Properties & Helper
|
||||||
# -----------------------
|
# -----------------------
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children(self) -> Dict['Record', List[Any]]:
|
def children(self) -> List['Record']:
|
||||||
''' Returns dict with page record key and (optional) extra value. '''
|
''' Returns dict with page record key and (optional) extra value. '''
|
||||||
return self._children
|
return self._children
|
||||||
|
|
||||||
@@ -119,14 +140,6 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
return iter(self._children).__next__()
|
return iter(self._children).__next__()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
|
||||||
def first_extra(self) -> Optional[Any]:
|
|
||||||
''' Returns first additional / extra info object of first page. '''
|
|
||||||
if not self._children:
|
|
||||||
return None
|
|
||||||
val = iter(self._children.values()).__next__()
|
|
||||||
return val[0] if val else 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'):
|
||||||
|
|||||||
@@ -66,28 +66,22 @@ class Watcher:
|
|||||||
for key, field in self._model_reader.read(record):
|
for key, field in self._model_reader.read(record):
|
||||||
_gen = self.callback(GroupByCallbackArgs(record, key, field))
|
_gen = self.callback(GroupByCallbackArgs(record, key, field))
|
||||||
try:
|
try:
|
||||||
obj = next(_gen)
|
group = next(_gen)
|
||||||
while True:
|
while True:
|
||||||
if not isinstance(obj, (str, tuple)):
|
if not isinstance(group, str):
|
||||||
raise TypeError(f'Unsupported groupby yield: {obj}')
|
raise TypeError(f'Unsupported groupby yield: {group}')
|
||||||
slug = self._persist(record, key, obj)
|
slug = self._persist(record, key, group)
|
||||||
# return slugified group key and continue iteration
|
# return slugified group key and continue iteration
|
||||||
if isinstance(_gen, Generator) and not _gen.gi_yieldfrom:
|
if isinstance(_gen, Generator) and not _gen.gi_yieldfrom:
|
||||||
obj = _gen.send(slug)
|
group = _gen.send(slug)
|
||||||
else:
|
else:
|
||||||
obj = next(_gen)
|
group = next(_gen)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
del _gen
|
del _gen
|
||||||
|
|
||||||
def _persist(
|
def _persist(self, record: 'Record', key: 'FieldKeyPath', group: str) \
|
||||||
self, record: 'Record', key: 'FieldKeyPath', obj: Union[str, tuple]
|
-> str:
|
||||||
) -> str:
|
|
||||||
''' Update internal state. Return slugified string. '''
|
''' Update internal state. Return slugified string. '''
|
||||||
if isinstance(obj, str):
|
|
||||||
group, extra = obj, key.fieldKey
|
|
||||||
else:
|
|
||||||
group, extra = obj
|
|
||||||
|
|
||||||
alt = record.alt
|
alt = record.alt
|
||||||
slug = self.config.slugify(group)
|
slug = self.config.slugify(group)
|
||||||
if slug not in self._state[alt]:
|
if slug not in self._state[alt]:
|
||||||
@@ -96,7 +90,7 @@ class Watcher:
|
|||||||
else:
|
else:
|
||||||
src = self._state[alt][slug]
|
src = self._state[alt][slug]
|
||||||
|
|
||||||
src.append_child(record, extra, group)
|
src.append_child(record, group)
|
||||||
# reverse reference
|
# reverse reference
|
||||||
VGroups.of(record).add(key, src)
|
VGroups.of(record).add(key, src)
|
||||||
return slug
|
return slug
|
||||||
|
|||||||
Reference in New Issue
Block a user