refactor: rename group -> key_obj
This commit is contained in:
@@ -2,8 +2,9 @@ from inifile import IniFile
|
|||||||
from lektor.environment import Expression
|
from lektor.environment import Expression
|
||||||
from lektor.context import Context
|
from lektor.context import Context
|
||||||
from lektor.utils import slugify as _slugify
|
from lektor.utils import slugify as _slugify
|
||||||
from typing import TYPE_CHECKING
|
from typing import (
|
||||||
from typing import Set, Dict, Optional, Union, Any, List, Generator
|
TYPE_CHECKING, Set, Dict, Optional, Union, Any, List, Generator
|
||||||
|
)
|
||||||
from .util import split_strip
|
from .util import split_strip
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from lektor.sourceobj import SourceObject
|
from lektor.sourceobj import SourceObject
|
||||||
@@ -44,14 +45,14 @@ class Config:
|
|||||||
slug: Optional[str] = None, # default: "{attr}/{group}/index.html"
|
slug: Optional[str] = None, # default: "{attr}/{group}/index.html"
|
||||||
template: Optional[str] = None, # default: "groupby-{attr}.html"
|
template: Optional[str] = None, # default: "groupby-{attr}.html"
|
||||||
replace_none_key: Optional[str] = None, # default: None
|
replace_none_key: Optional[str] = None, # default: None
|
||||||
key_map_fn: Optional[str] = None, # default: None
|
key_obj_fn: Optional[str] = None, # default: None
|
||||||
) -> None:
|
) -> None:
|
||||||
self.key = key
|
self.key = key
|
||||||
self.root = (root or '/').rstrip('/') or '/'
|
self.root = (root or '/').rstrip('/') or '/'
|
||||||
self.slug = slug or (key + '/{key}/') # key = GroupBySource.key
|
self.slug = slug or (key + '/{key}/') # key = GroupBySource.key
|
||||||
self.template = template or f'groupby-{self.key}.html'
|
self.template = template or f'groupby-{self.key}.html'
|
||||||
self.replace_none_key = replace_none_key
|
self.replace_none_key = replace_none_key
|
||||||
self.key_map_fn = key_map_fn
|
self.key_obj_fn = key_obj_fn
|
||||||
# editable after init
|
# editable after init
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
self.dependencies = set() # type: Set[str]
|
self.dependencies = set() # type: Set[str]
|
||||||
@@ -98,7 +99,7 @@ class Config:
|
|||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
txt = '<GroupByConfig'
|
txt = '<GroupByConfig'
|
||||||
for x in ['enabled', 'key', 'root', 'slug', 'template', 'key_map_fn']:
|
for x in ['enabled', 'key', 'root', 'slug', 'template', 'key_obj_fn']:
|
||||||
txt += ' {}="{}"'.format(x, getattr(self, x))
|
txt += ' {}="{}"'.format(x, getattr(self, x))
|
||||||
txt += f' fields="{", ".join(self.fields)}"'
|
txt += f' fields="{", ".join(self.fields)}"'
|
||||||
if self.order_by:
|
if self.order_by:
|
||||||
@@ -114,7 +115,7 @@ class Config:
|
|||||||
slug=cfg.get('slug'),
|
slug=cfg.get('slug'),
|
||||||
template=cfg.get('template'),
|
template=cfg.get('template'),
|
||||||
replace_none_key=cfg.get('replace_none_key'),
|
replace_none_key=cfg.get('replace_none_key'),
|
||||||
key_map_fn=cfg.get('key_map_fn'),
|
key_obj_fn=cfg.get('key_obj_fn'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -184,13 +185,13 @@ class Config:
|
|||||||
expr = self._make_expression(cfg_slug, on=on, field='slug')
|
expr = self._make_expression(cfg_slug, on=on, field='slug')
|
||||||
return expr.evaluate(on.pad, this=on, alt=on.alt) or None
|
return expr.evaluate(on.pad, this=on, alt=on.alt) or None
|
||||||
|
|
||||||
def eval_key_map_fn(self, *, on: 'SourceObject', context: Dict) -> Any:
|
def eval_key_obj_fn(self, *, on: 'SourceObject', context: Dict) -> Any:
|
||||||
'''
|
'''
|
||||||
If `key_map_fn` is set, evaluate field expression.
|
If `key_obj_fn` is set, evaluate field expression.
|
||||||
Note: The function does not check whether `key_map_fn` is set.
|
Note: The function does not check whether `key_obj_fn` is set.
|
||||||
Return: A Generator result is automatically unpacked into a list.
|
Return: A Generator result is automatically unpacked into a list.
|
||||||
'''
|
'''
|
||||||
exp = self._make_expression(self.key_map_fn, on=on, field='key_map_fn')
|
exp = self._make_expression(self.key_obj_fn, on=on, field='key_obj_fn')
|
||||||
with Context(pad=on.pad) as ctx:
|
with Context(pad=on.pad) as ctx:
|
||||||
with ctx.gather_dependencies(self.dependencies.add):
|
with ctx.gather_dependencies(self.dependencies.add):
|
||||||
res = exp.evaluate(on.pad, this=on, alt=on.alt, values=context)
|
res = exp.evaluate(on.pad, this=on, alt=on.alt, values=context)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from lektor.db import Page # isinstance
|
from lektor.db import Page # isinstance
|
||||||
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Iterable
|
from typing import (
|
||||||
|
TYPE_CHECKING, NamedTuple, Dict, List, Any, Optional, Iterable
|
||||||
|
)
|
||||||
from .util import build_url
|
from .util import build_url
|
||||||
from .vobj import VPATH, GroupBySource
|
from .vobj import VPATH, GroupBySource
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -9,16 +11,16 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class ResolverEntry(NamedTuple):
|
class ResolverEntry(NamedTuple):
|
||||||
slug: str
|
key: str
|
||||||
group: str
|
key_obj: Any
|
||||||
config: 'Config'
|
config: 'Config'
|
||||||
page: Optional[int]
|
page: Optional[int]
|
||||||
|
|
||||||
def equals(
|
def equals(
|
||||||
self, path: str, attribute: str, group: str, page: Optional[int]
|
self, path: str, conf_key: str, vobj_key: str, page: Optional[int]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return self.slug == group \
|
return self.key == vobj_key \
|
||||||
and self.config.key == attribute \
|
and self.config.key == conf_key \
|
||||||
and self.config.root == path \
|
and self.config.root == path \
|
||||||
and self.page == page
|
and self.page == page
|
||||||
|
|
||||||
@@ -53,9 +55,9 @@ class Resolver:
|
|||||||
def add(self, vobj: GroupBySource) -> None:
|
def add(self, vobj: GroupBySource) -> None:
|
||||||
''' Track new virtual object (only if slug is set). '''
|
''' Track new virtual object (only if slug is set). '''
|
||||||
if vobj.slug:
|
if vobj.slug:
|
||||||
# page_num = 1 overwrites page_num = None -> same url_path()
|
# `page_num = 1` overwrites `page_num = None` -> same url_path()
|
||||||
self._data[vobj.url_path] = ResolverEntry(
|
self._data[vobj.url_path] = ResolverEntry(
|
||||||
vobj.key, vobj.group, vobj.config, vobj.page_num)
|
vobj.key, vobj.key_obj, vobj.config, vobj.page_num)
|
||||||
|
|
||||||
# ------------
|
# ------------
|
||||||
# Resolver
|
# Resolver
|
||||||
@@ -68,15 +70,16 @@ class Resolver:
|
|||||||
rv = self._data.get(build_url([node.url_path] + pieces))
|
rv = self._data.get(build_url([node.url_path] + pieces))
|
||||||
if rv:
|
if rv:
|
||||||
return GroupBySource(
|
return GroupBySource(
|
||||||
node, rv.slug, rv.page).finalize(rv.config, rv.group)
|
node, rv.key, rv.page).finalize(rv.config, rv.key_obj)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def resolve_virtual_path(self, node: 'SourceObject', pieces: List[str]) \
|
def resolve_virtual_path(self, node: 'SourceObject', pieces: List[str]) \
|
||||||
-> Optional[GroupBySource]:
|
-> Optional[GroupBySource]:
|
||||||
''' Admin UI only: Prevent server error and null-redirect. '''
|
''' Admin UI only: Prevent server error and null-redirect. '''
|
||||||
|
# format: /path/to/page@groupby/{config-key}/{vobj-key}/{page-num}
|
||||||
if isinstance(node, Page) and len(pieces) >= 2:
|
if isinstance(node, Page) and len(pieces) >= 2:
|
||||||
path = node['_path'] # type: str
|
path = node['_path'] # type: str
|
||||||
attr, grp, *optional_page = pieces
|
conf_key, vobj_key, *optional_page = pieces
|
||||||
page = None
|
page = None
|
||||||
if optional_page:
|
if optional_page:
|
||||||
try:
|
try:
|
||||||
@@ -84,7 +87,7 @@ class Resolver:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
for rv in self._data.values():
|
for rv in self._data.values():
|
||||||
if rv.equals(path, attr, grp, page):
|
if rv.equals(path, conf_key, vobj_key, page):
|
||||||
return GroupBySource(
|
return GroupBySource(
|
||||||
node, rv.slug, rv.page).finalize(rv.config, rv.group)
|
node, rv.key, rv.page).finalize(rv.config, rv.key_obj)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ from lektor.context import get_ctx
|
|||||||
from lektor.db import _CmpHelper
|
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 typing import TYPE_CHECKING
|
from typing import (
|
||||||
from typing import List, Any, Dict, Optional, Generator, Iterator, Iterable
|
TYPE_CHECKING, List, Any, Dict, Optional, Generator, Iterator, Iterable
|
||||||
|
)
|
||||||
from .pagination import PaginationConfig
|
from .pagination import PaginationConfig
|
||||||
from .query import FixedRecordsQuery
|
from .query import FixedRecordsQuery
|
||||||
from .util import most_used_key, insert_before_ext, build_url, cached_property
|
from .util import most_used_key, insert_before_ext, build_url, cached_property
|
||||||
@@ -25,31 +26,32 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
'''
|
'''
|
||||||
Holds information for a single group/cluster.
|
Holds information for a single group/cluster.
|
||||||
This object is accessible in your template file.
|
This object is accessible in your template file.
|
||||||
Attributes: record, key, group, slug, children, config
|
Attributes: record, key, key_obj, slug, children, config
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
record: 'Record',
|
record: 'Record',
|
||||||
slug: str,
|
key: str,
|
||||||
page_num: Optional[int] = None
|
page_num: Optional[int] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(record)
|
super().__init__(record)
|
||||||
self.key = slug
|
|
||||||
self.page_num = page_num
|
|
||||||
self._expr_fields = {} # type: Dict[str, Expression]
|
|
||||||
self.__children = [] # type: List[str]
|
self.__children = [] # type: List[str]
|
||||||
self.__group_map = [] # type: List[Any]
|
self.__key_obj_map = [] # type: List[Any]
|
||||||
|
self._expr_fields = {} # type: Dict[str, Expression]
|
||||||
|
self.key = key
|
||||||
|
self.page_num = page_num
|
||||||
|
|
||||||
def append_child(self, child: 'Record', group: Any) -> None:
|
def append_child(self, child: 'Record', key_obj: Any) -> None:
|
||||||
if child not in self.__children:
|
if child not in self.__children:
|
||||||
self.__children.append(child.path)
|
self.__children.append(child.path)
|
||||||
# TODO: rename group to value
|
# __key_obj_map is later used to find most used key_obj
|
||||||
# __group_map is later used to find most used group
|
self.__key_obj_map.append(key_obj)
|
||||||
self.__group_map.append(group)
|
|
||||||
|
|
||||||
def _update_attr(self, key: str, value: Any) -> None:
|
def _update_attr(self, key: str, value: Any) -> None:
|
||||||
''' Set or remove Jinja evaluated Expression field. '''
|
''' Set or remove Jinja evaluated Expression field. '''
|
||||||
|
# TODO: instead we could evaluate the fields only once.
|
||||||
|
# But then we need to record_dependency() every successive access
|
||||||
if isinstance(value, Expression):
|
if isinstance(value, Expression):
|
||||||
self._expr_fields[key] = value
|
self._expr_fields[key] = value
|
||||||
try:
|
try:
|
||||||
@@ -65,22 +67,22 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
# Evaluate Extra Fields
|
# Evaluate Extra Fields
|
||||||
# -------------------------
|
# -------------------------
|
||||||
|
|
||||||
def finalize(self, config: 'Config', group: Optional[Any] = None) \
|
def finalize(self, config: 'Config', key_obj: Optional[Any] = None) \
|
||||||
-> 'GroupBySource':
|
-> 'GroupBySource':
|
||||||
self.config = config
|
self.config = config
|
||||||
# make a sorted children query
|
# make a sorted children query
|
||||||
self._query = FixedRecordsQuery(self.pad, self.__children, self.alt)
|
self._query = FixedRecordsQuery(self.pad, self.__children, self.alt)
|
||||||
self._query._order_by = config.order_by
|
self._query._order_by = config.order_by
|
||||||
del self.__children
|
del self.__children
|
||||||
# set group name
|
# set indexed original value (can be: str, int, float, bool, obj)
|
||||||
self.group = group or most_used_key(self.__group_map)
|
self.key_obj = key_obj or most_used_key(self.__key_obj_map)
|
||||||
del self.__group_map
|
del self.__key_obj_map
|
||||||
# evaluate slug Expression
|
# evaluate slug Expression
|
||||||
self.slug = config.eval_slug(self.key, on=self)
|
self.slug = config.eval_slug(self.key, on=self)
|
||||||
if self.slug and self.slug.endswith('/index.html'):
|
if self.slug and self.slug.endswith('/index.html'):
|
||||||
self.slug = self.slug[:-10]
|
self.slug = self.slug[:-10]
|
||||||
|
|
||||||
if group: # exit early if initialized through resolver
|
if key_obj: # exit early if initialized through resolver
|
||||||
return self
|
return self
|
||||||
# extra fields
|
# extra fields
|
||||||
for attr in config.fields:
|
for attr in config.fields:
|
||||||
@@ -202,15 +204,15 @@ class GroupBySource(VirtualSourceObject):
|
|||||||
raise AttributeError
|
raise AttributeError
|
||||||
|
|
||||||
def __lt__(self, other: 'GroupBySource') -> bool:
|
def __lt__(self, other: 'GroupBySource') -> bool:
|
||||||
# Used for |sort filter ("group" is the provided original string)
|
# Used for |sort filter (`key_obj` is the indexed original value)
|
||||||
if isinstance(self.group, (bool, int, float)) and \
|
if isinstance(self.key_obj, (bool, int, float)) and \
|
||||||
isinstance(other.group, (bool, int, float)):
|
isinstance(other.key_obj, (bool, int, float)):
|
||||||
return self.group < other.group
|
return self.key_obj < other.key_obj
|
||||||
if self.group is None:
|
if self.key_obj is None:
|
||||||
return False
|
return False # this will sort None at the end
|
||||||
if other.group is None:
|
if other.key_obj is None:
|
||||||
return True
|
return True
|
||||||
return str(self.group).lower() < str(other.group).lower()
|
return str(self.key_obj).lower() < str(other.key_obj).lower()
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
# Used for |unique filter
|
# Used for |unique filter
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from typing import TYPE_CHECKING, Dict, List, Any, Union, NamedTuple
|
from typing import (
|
||||||
from typing import Optional, Callable, Iterator, Generator
|
TYPE_CHECKING, Dict, List, Any, Union, NamedTuple,
|
||||||
|
Optional, Callable, Iterator, Generator
|
||||||
|
)
|
||||||
from .backref import VGroups
|
from .backref import VGroups
|
||||||
from .model import ModelReader
|
from .model import ModelReader
|
||||||
from .vobj import GroupBySource
|
from .vobj import GroupBySource
|
||||||
@@ -24,7 +26,7 @@ GroupingCallback = Callable[[GroupByCallbackArgs], Union[
|
|||||||
class Watcher:
|
class Watcher:
|
||||||
'''
|
'''
|
||||||
Callback is called with (Record, FieldKeyPath, field-value).
|
Callback is called with (Record, FieldKeyPath, field-value).
|
||||||
Callback may yield one or more (group, extra-info) tuples.
|
Callback may yield 0-n objects.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, config: 'Config') -> None:
|
def __init__(self, config: 'Config') -> None:
|
||||||
@@ -37,7 +39,7 @@ class Watcher:
|
|||||||
Decorator to subscribe to attrib-elements.
|
Decorator to subscribe to attrib-elements.
|
||||||
If flatten = False, dont explode FlowType.
|
If flatten = False, dont explode FlowType.
|
||||||
|
|
||||||
(record, field-key, field) -> (group, extra-info)
|
(record, field-key, field) -> value
|
||||||
'''
|
'''
|
||||||
def _decorator(fn: GroupingCallback) -> None:
|
def _decorator(fn: GroupingCallback) -> None:
|
||||||
self.flatten = flatten
|
self.flatten = flatten
|
||||||
@@ -68,25 +70,25 @@ class Watcher:
|
|||||||
args = GroupByCallbackArgs(record, key, field)
|
args = GroupByCallbackArgs(record, key, field)
|
||||||
_gen = self.callback(args)
|
_gen = self.callback(args)
|
||||||
try:
|
try:
|
||||||
group = next(_gen)
|
key_obj = next(_gen)
|
||||||
while True:
|
while True:
|
||||||
if self.config.key_map_fn:
|
if self.config.key_obj_fn:
|
||||||
slug = self._persist_multiple(args, group)
|
slug = self._persist_multiple(args, key_obj)
|
||||||
else:
|
else:
|
||||||
slug = self._persist(args, group)
|
slug = self._persist(args, key_obj)
|
||||||
# return slugified group key and continue iteration
|
# return slugified key and continue iteration
|
||||||
if isinstance(_gen, Generator) and not _gen.gi_yieldfrom:
|
if isinstance(_gen, Generator) and not _gen.gi_yieldfrom:
|
||||||
group = _gen.send(slug)
|
key_obj = _gen.send(slug)
|
||||||
else:
|
else:
|
||||||
group = next(_gen)
|
key_obj = next(_gen)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
del _gen
|
del _gen
|
||||||
|
|
||||||
def _persist_multiple(self, args: 'GroupByCallbackArgs', obj: Any) \
|
def _persist_multiple(self, args: 'GroupByCallbackArgs', obj: Any) \
|
||||||
-> Optional[str]:
|
-> Optional[str]:
|
||||||
# if custom key mapping function defined, use that first
|
# if custom key mapping function defined, use that first
|
||||||
res = self.config.eval_key_map_fn(on=args.record,
|
res = self.config.eval_key_obj_fn(on=args.record,
|
||||||
context={'X': obj, 'SRC': args})
|
context={'X': obj, 'ARGS': args})
|
||||||
if isinstance(res, (list, tuple)):
|
if isinstance(res, (list, tuple)):
|
||||||
for k in res:
|
for k in res:
|
||||||
self._persist(args, k) # 1-to-n replacement
|
self._persist(args, k) # 1-to-n replacement
|
||||||
|
|||||||
Reference in New Issue
Block a user