class AutoConfig(RefinableObject): model: Type[Model] = Refinable() # model is evaluated, but in a special way so gets no EvaluatedRefinable type include = Refinable() exclude = Refinable() @dispatch def __init__(self, **kwargs): super(AutoConfig, self).__init__(**kwargs)
class MyClass(RefinableObject): foo = Refinable() container = Refinable() def bind(self, container): new_object = copy.copy(self) new_object.container = container return new_object
class MyClass(RefinableObject): foo = Refinable() container = Refinable() # noinspection PyShadowingNames def bind(self, container): new_object = copy.copy(self) new_object.container = container return new_object
class MyClass(RefinableObject): x = Refinable() y = Refinable() @dispatch( x=17, y=EMPTY, ) def __init__(self, **kwargs): super(MyClass, self).__init__(**kwargs)
class A(RefinableObject): foo = Refinable() bar = Refinable() @classmethod @class_shortcut def shortcut1(cls, call_target, **kwargs): return call_target(**kwargs) def items(self): return dict(foo=self.foo, bar=self.bar)
class Foo(RefinableObject): """ docstring for Foo """ name = Refinable() description = Refinable() some_other_thing = Refinable() empty_string_default = Refinable() @dispatch( name='foo-name', description=lambda foo, bar: 'qwe', some_other_thing=some_callable, empty_string_default='', ) def __init__(self): """ :param name: description of the name field """ super(Foo, self).__init__() # pragma: no cover @staticmethod @refinable def refinable_func(field, instance, value): pass # pragma: no cover @classmethod @class_shortcut def shortcut1(cls): return cls() # pragma: no cover @classmethod @class_shortcut(description='fish') def shortcut2(cls, call_target): """shortcut2 docstring""" return call_target() # pragma: no cover @classmethod # fmt: off @class_shortcut(description=lambda foo: 'qwe') # fmt: on def shortcut3(cls, call_target): """ shortcut3 docstring :param call_target: something something call_target """ return call_target() # pragma: no cover
class Foo(RefinableObject): """docstring for Foo""" name = Refinable() description = Refinable() some_other_thing = Refinable() @dispatch( name='foo-name', description=lambda foo, bar: 'qwe', some_other_thing=some_callable, ) def __init__(self): """ :param name: description of the name field """ super(Foo, self).__init__() @staticmethod @refinable def refinable_func(field, instance, value): pass @classmethod @class_shortcut def shortcut1(cls): return cls() @classmethod @class_shortcut( description='fish' ) def shortcut2(cls, call_target): """shortcut2 docstring""" return call_target() @classmethod @class_shortcut( description=lambda foo: 'qwe' # TODO: This is currently not shown in the documentation output, but it should be! ) def shortcut3(cls, call_target): """ shortcut3 docstring :param call_target: something something call_target """ return call_target()
class A(Traversable): @dispatch @reinvokable def __init__(self, **kwargs): super().__init__(**kwargs) foo = Refinable() bar = Refinable() @classmethod @class_shortcut def shortcut1(cls, call_target, **kwargs): return call_target(**kwargs) def items(self): return dict(foo=self.foo, bar=self.bar)
class ReinvokableWithExtra(Namespace): _name = None extra = Refinable() @reinvokable def __init__(self, **kwargs): super().__init__(**kwargs)
class Foo(RefinableObject): name = Refinable() @dispatch( # this is to handle that mutmut mutates strip(',') to strip('XX,XX') name=lambda X: X, ) def __init__(self, **kwargs): super(Foo, self).__init__(**kwargs) # pragma: no cover
class Endpoint(Traversable): """ Class that describes an endpoint in iommi. You can create your own custom endpoints on any :doc:`Part`. Example: .. code:: python def my_view(request): return Page( parts__h1=html.h1('Hi!'), endpoints__echo__func=lambda value, **_: value, ) .. test import json request = req('get', **{'/echo': 'foo'}) response = my_view(request).bind(request=request).render_to_response() assert json.loads(response.content) == 'foo' this page will respond to `?/echo=foo` by returning a json response `"foo"`. """ name: str = Refinable() func: Callable = Refinable() include: bool = EvaluatedRefinable() @dispatch( name=None, func=None, include=True, ) def __init__(self, **kwargs): super(Endpoint, self).__init__(**kwargs) def on_bind(self) -> None: assert callable(self.func) @property def endpoint_path(self): return DISPATCH_PREFIX + self.iommi_path def own_evaluate_parameters(self): return dict(endpoint=self)
class Fragment(PagePart): attrs: Dict[str, Any] = Refinable() tag = Refinable() template: Union[str, Template] = Refinable() @dispatch( attrs=EMPTY, tag=None, ) def __init__(self, child: PartType = None, *, children: Optional[List[PartType]] = None, **kwargs): super(Fragment, self).__init__(**kwargs) self._children = [ ] # TODO: _children to avoid colliding with PageParts children() API. Not nice. We should do something nicer here. if child is not None: self._children.append(child) self._children.extend(children or []) def render_text_or_children(self, context): return format_html( '{}' * len(self._children), *[as_html(part=x, context=context) for x in self._children]) def __repr__(self): return f'<Fragment: tag:{self.tag}, attrs:{self.attrs.items()}>' def on_bind(self) -> None: self.attrs = evaluate_attrs(self, **self.evaluate_attribute_kwargs()) # TODO: do we want to do this? # self._children = [evaluate_strict(x, **self.evaluate_attribute_kwargs()) for x in self._children] @dispatch( context=EMPTY, render=fragment__render, ) def __html__(self, *, context=None, render=None): return render(fragment=self, context=context) def _evaluate_attribute_kwargs(self): return dict(fragment=self)
class MenuBase(Part): tag: str = EvaluatedRefinable() sort: bool = EvaluatedRefinable() # only applies for submenu items sub_menu: Dict = Refinable() attrs: Attrs = Refinable( ) # attrs is evaluated, but in a special way so gets no EvaluatedRefinable type template: Union[str, Template] = EvaluatedRefinable() @reinvokable @dispatch( sort=True, sub_menu=EMPTY, attrs__class=EMPTY, attrs__style=EMPTY, ) def __init__(self, sub_menu, _sub_menu_dict=None, **kwargs): super(MenuBase, self).__init__(**kwargs) self._active = False collect_members( self, name='sub_menu', items=sub_menu, # TODO: cls=self.get_meta().member_class, items_dict=_sub_menu_dict, cls=MenuItem, ) def __repr__(self): r = f'{self._name}' if self.sub_menu: for items in values(self.sub_menu): r += ''.join([f'\n {x}' for x in repr(items).split('\n')]) return r def on_bind(self): bind_members(self, name='sub_menu') if self.sort: self.sub_menu = Struct({ item._name: item for item in sorted(values(self.sub_menu), key=lambda x: x.display_name) })
class Actions(Members, Tag): attrs: Attrs = Refinable( ) # attrs is evaluated, but in a special way so gets no EvaluatedRefinable type tag = EvaluatedRefinable() @dispatch( attrs__class=EMPTY, attrs__style=EMPTY, ) def __init__(self, **kwargs): super(Actions, self).__init__(**kwargs)
class Fruit(Traversable): attrs = Refinable() @reinvokable @dispatch( attrs__class__fruit=True, ) def __init__(self, **kwargs): super(Fruit, self).__init__(**kwargs) @classmethod @class_shortcut( attrs__class__some_fruit=True, ) def some_fruit(cls, *, call_target=None, **kwargs): return call_target(**kwargs)
class Foo(RefinableObject): a = Refinable() b = Refinable() @dispatch( b='default_b', ) def __init__(self, **kwargs): self.non_refinable = 17 super(Foo, self).__init__(**kwargs) @staticmethod @dispatch(f=Namespace(call_target=f_)) @refinable def c(f): """ c docstring """ return f() @staticmethod @shortcut @dispatch(call_target=f_) def shortcut_to_f(call_target): return call_target()
class Foo(RefinableObject): """ First description """ name = Refinable() @dispatch def __init__(self, **kwargs): """ __init__ description :param foo: foo description """ super(Foo, self).__init__(**kwargs)
class Basket(Traversable): fruits = Refinable() class Meta: fruits__banana__attrs__class__basket = True @dispatch( fruits=EMPTY, ) def __init__(self, *, _fruits_dict, fruits, **kwargs): super(Basket, self).__init__(**kwargs) collect_members(self, name='fruits', items_dict=_fruits_dict, items=fruits, cls=Fruit) def on_bind(self) -> None: bind_members(self, name='fruits')
class Advanced(Fragment): toggle: Namespace = Refinable() @dispatch(toggle=EMPTY) @reinvokable def __init__(self, **kwargs): super(Advanced, self).__init__(**kwargs) toggle = setdefaults_path( Namespace(), self.toggle, _name='toggle', call_target=Action, attrs__href='#', attrs__class__iommi_query_toggle_simple_mode=True, attrs={'data-advanced-mode': 'simple'}, display_name=gettext('Switch to advanced search'), ) self.toggle = declared_members(self).toggle = toggle() def on_bind(self) -> None: super(Advanced, self).on_bind() self.toggle = self._bound_members.toggle = self.toggle.bind(parent=self)
class Action(Fragment): """ The `Action` class describes buttons and links. Examples: .. code:: python # Link Action(attrs__href='http://example.com') # Link with icon Action.icon('edit', attrs__href="edit/") # Button Action.button(display_name='Button title!') # A submit button Action.submit(display_name='Do this') # The primary submit button on a form. Action.primary() # Notice that because forms # with a single primary submit button are so common, iommi assumes # that if you have a action called submit and do NOT explicitly # specify the action that it is a primary action. This is only # done for the action called submit, inside the Forms actions # Namespace. # # For that reason this works: class MyForm(Form): class Meta: @staticmethod def actions__submit__post_handler(form, **_): if not form.is_valid(): return ... # and is roughly equivalent to def on_submit(form, **_): if not form.is_valid(): return class MyOtherForm(Form): class Meta: actions__submit = Action.primary(post_handler=on_submit) .. test r = req('post', **{'-submit': ''}) MyForm().bind(request=r).render_to_response() MyOtherForm().bind(request=r).render_to_response() """ group: str = EvaluatedRefinable() display_name: str = EvaluatedRefinable() post_handler: Callable = Refinable() @dispatch( tag='a', attrs=EMPTY, children=EMPTY, display_name=lambda action, **_: capitalize(action._name).replace( '_', ' '), ) @reinvokable def __init__(self, *, tag=None, attrs=None, children=None, display_name=None, **kwargs): if tag == 'input': if display_name and 'value' not in attrs: attrs.value = display_name else: children['text'] = display_name if tag == 'button' and 'value' in attrs: assert False, 'You passed attrs__value, but you should pass display_name' super().__init__(tag=tag, attrs=attrs, children=children, display_name=display_name, **kwargs) def on_bind(self): super().on_bind() def own_evaluate_parameters(self): return dict(action=self) def __repr__(self): return Part.__repr__(self) def own_target_marker(self): return f'-{self.iommi_path}' def is_target(self): return self.own_target_marker() in self.iommi_parent().iommi_parent( )._request_data @classmethod @class_shortcut( tag='button', ) def button(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='button', attrs__accesskey='s', attrs__name=lambda action, **_: action.own_target_marker(), display_name=gettext_lazy('Submit'), ) def submit(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='submit', ) def primary(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='submit', ) def delete(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( icon_classes=[], ) def icon(cls, icon, *, display_name=None, call_target=None, icon_classes=None, **kwargs): icon_classes_str = ' '.join( ['fa-' + icon_class for icon_class in icon_classes]) if icon_classes else '' if icon_classes_str: icon_classes_str = ' ' + icon_classes_str setdefaults_path( kwargs, display_name=format_html('<i class="fa fa-{}{}"></i> {}', icon, icon_classes_str, display_name), ) return call_target(**kwargs)
class Traversable(RefinableObject): """ Abstract API for objects that have a place in the iommi path structure. You should not need to care about this class as it is an implementation detail. """ _name = None _parent = None _is_bound = False _request = None context = None iommi_style: str = Refinable() _declared_members: Dict[str, 'Traversable'] _bound_members: Dict[str, 'Traversable'] @dispatch def __init__(self, _name=None, **kwargs): self._declared_members = Struct() self._bound_members = Struct() self._evaluate_parameters = None self._name = _name super(Traversable, self).__init__(**kwargs) def __repr__(self): n = f' {self._name}' if self._name is not None else '' b = ' (bound)' if self._is_bound else '' try: p = f" path:'{self.iommi_path}'" if self.iommi_parent( ) is not None else "" except PathNotFoundException: p = ' path:<no path>' c = '' if self._is_bound and hasattr(self, '_bound_members'): members = self._bound_members if members: c = f" members:{list(members.keys())!r}" return f'<{type(self).__module__}.{type(self).__name__}{n}{b}{p}{c}>' def iommi_name(self) -> str: return self._name def iommi_parent(self) -> "Traversable": return self._parent def iommi_root(self) -> 'Traversable': node = self while node.iommi_parent() is not None: node = node.iommi_parent() return node def iommi_bound_members(self) -> Dict[str, 'Traversable']: return self._bound_members if self._bound_members is not None else Struct( ) @property def iommi_path(self) -> str: long_path = build_long_path(self) path_by_long_path = get_path_by_long_path(self) path = path_by_long_path.get(long_path) if path is None: candidates = '\n'.join(path_by_long_path.keys()) raise PathNotFoundException( f"Path not found(!) (Searched for {long_path} among the following:\n{candidates}" ) return path @property def iommi_dunder_path(self) -> str: assert self._is_bound return build_long_path(self).replace('/', '__') def reinvoke(self, additional_kwargs: Dict[str, Any]) -> "Traversable": assert hasattr( self, '_iommi_saved_params' ), f'reinvoke() called on class with missing @reinvokable decorator: {self.__class__.__name__}' additional_kwargs_namespace = Namespace(additional_kwargs) kwargs = {} for name, saved_param in items(self._iommi_saved_params): try: new_param = getattr_path(additional_kwargs_namespace, name) except AttributeError: kwargs[name] = saved_param else: if hasattr(saved_param, 'reinvoke'): assert isinstance(new_param, dict) kwargs[name] = saved_param.reinvoke(new_param) else: if isinstance(saved_param, Namespace): kwargs[name] = Namespace(saved_param, new_param) else: kwargs[name] = new_param additional_kwargs_namespace.pop('call_target', None) kwargs = Namespace( additional_kwargs_namespace, kwargs) # Also include those keys not already in the original result = type(self)(**kwargs) result._name = self._name __tri_declarative_shortcut_stack = getattr( self, '__tri_declarative_shortcut_stack', None) if __tri_declarative_shortcut_stack is not None: setattr(result, '__tri_declarative_shortcut_stack', __tri_declarative_shortcut_stack) return result def bind(self, *, parent=None, request=None): assert parent is None or parent._is_bound assert not self._is_bound if parent is None: self._request = request if self._name is None: self._name = 'root' result = copy.copy(self) result._declared = self del self # to prevent mistakes when changing the code below result._parent = parent result._is_bound = True evaluate_parameters = { 'traversable': result, **(parent.iommi_evaluate_parameters() if parent is not None else {}), **result.own_evaluate_parameters(), } if parent is None: evaluate_parameters['request'] = request result._evaluate_parameters = evaluate_parameters if hasattr(result, 'include'): include = evaluate_strict(result.include, **evaluate_parameters) if not bool(include): return None else: include = MISSING if include is not MISSING: result.include = True rest_of_style = apply_style(result) # Styling has another chance of setting include to False if include is not MISSING and result.include is False: return None result.include = True result.on_bind() if rest_of_style: rest = apply_style_recursively(style_data=rest_of_style, obj=result) assert not rest, f'There is still styling data left for {result}: {rest_of_style}' # on_bind has a chance to hide itself if result.include is False: return None if hasattr(result, 'attrs'): result.attrs = evaluate_attrs(result, **result.iommi_evaluate_parameters()) evaluated_attributes = [ k for k, v in items(result.get_declared('refinable_members')) if is_evaluated_refinable(v) ] evaluate_members(result, evaluated_attributes, **evaluate_parameters) if hasattr(result, 'extra_evaluated'): result.extra_evaluated = evaluate_strict_container( result.extra_evaluated or {}, **evaluate_parameters) return result def on_bind(self) -> None: pass def own_evaluate_parameters(self): return {} def iommi_evaluate_parameters(self): return self._evaluate_parameters def get_request(self): if self._parent is None: return self._request else: return self.iommi_root().get_request() def get_context(self): if self._parent is None: return self.context or {} else: return self.iommi_parent().get_context()
class Foo(RefinableObject): name = Refinable()
class Query(Part): """ Declare a query language. Example: .. code:: python class CarQuery(Query): make = Filter.choice(choices=['Toyota', 'Volvo', 'Ford']) model = Filter() query_set = Car.objects.filter( CarQuery().bind(request=request).get_q() ) """ form: Namespace = Refinable() model: Type[Model] = Refinable( ) # model is evaluated, but in a special way so gets no EvaluatedRefinable type rows = Refinable() template: Union[str, Template] = EvaluatedRefinable() form_container: Fragment = EvaluatedRefinable() member_class = Refinable() form_class = Refinable() class Meta: member_class = Filter form_class = Form @reinvokable @dispatch( endpoints__errors__func=default_endpoint__errors, filters=EMPTY, auto=EMPTY, form__attrs={ 'data-iommi-errors': lambda query, **_: query.endpoints.errors.iommi_path }, form_container__call_target=Fragment, form_container__tag='span', form_container__attrs__class__iommi_query_form_simple=True, ) def __init__(self, *, model=None, rows=None, filters=None, _filters_dict=None, auto, **kwargs): assert isinstance(filters, dict) if auto: auto = QueryAutoConfig(**auto) auto_model, auto_rows, filters = self._from_model( model=auto.model, rows=auto.rows, filters=filters, include=auto.include, exclude=auto.exclude, ) assert model is None, "You can't use the auto feature and explicitly pass model. " \ "Either pass auto__model, or we will set the model for you from auto__rows" model = auto_model if rows is None: rows = auto_rows model, rows = model_and_rows(model, rows) setdefaults_path( kwargs, form__call_target=self.get_meta().form_class, ) self._form = None self.query_advanced_value = None self.query_error = None super(Query, self).__init__(model=model, rows=rows, **kwargs) collect_members(self, name='filters', items=filters, items_dict=_filters_dict, cls=self.get_meta().member_class) field_class = self.get_meta().form_class.get_meta().member_class declared_fields = Struct() declared_fields[FREETEXT_SEARCH_NAME] = field_class( _name=FREETEXT_SEARCH_NAME, display_name='Search', required=False, include=False, ) for name, filter in items(declared_members(self).filters): if filter.attr is None and getattr(filter.value_to_q, 'iommi_needs_attr', False): continue field = setdefaults_path( Namespace(), filter.field, _name=name, model_field=filter.model_field, attr=name if filter.attr is MISSING else filter.attr, call_target__cls=field_class, ) declared_fields[name] = field() # noinspection PyCallingNonCallable self.form: Form = self.form( _name='form', _fields_dict=declared_fields, attrs__method='get', actions__submit__attrs__value='Filter', ) declared_members(self).form = self.form self.advanced_simple_toggle = Action( attrs__href='#', attrs__class__iommi_query_toggle_simple_mode=True, attrs={'data-advanced-mode': 'simple'}, display_name='Switch to advanced search', ) self.form_container = self.form_container() # Filters need to be at the end to not steal the short names set_declared_member(self, 'filters', declared_members(self).pop('filters')) @dispatch( render__call_target=render_template, ) def __html__(self, *, render=None): if not self.iommi_bound_members().filters._bound_members: return '' setdefaults_path( render, context=self.iommi_evaluate_parameters(), template=self.template, ) return render(request=self.get_request()) def on_bind(self) -> None: bind_members(self, name='filters') self.advanced_simple_toggle = self.advanced_simple_toggle.bind( parent=self) request = self.get_request() self.query_advanced_value = request_data(request).get( self.get_advanced_query_param(), '') if request else '' # TODO: should it be possible to have freetext as a callable? this code just treats callables as truthy if any(f.freetext for f in values(declared_members(self)['filters'])): declared_members( self.form).fields[FREETEXT_SEARCH_NAME].include = True declared_fields = declared_members(self.form)['fields'] for name, filter in items(self.filters): assert filter.attr or not getattr( filter.value_to_q, 'iommi_needs_attr', False ), f"{name} cannot be a part of a query, it has no attr or value_to_q so we don't know what to search for" if name in declared_fields: field = setdefaults_path( Namespace(), _name=name, attr=name if filter.attr is MISSING else filter.attr, model_field=filter.model_field, help__include=False, ) declared_fields[name] = declared_fields[name].reinvoke(field) set_declared_member(self.form, 'fields', declared_fields) for name, field in items(declared_fields): if name == FREETEXT_SEARCH_NAME: continue if name not in self.filters: field.include = False bind_members(self, name='endpoints') self.form = self.form.bind(parent=self) self._bound_members.form = self.form self.form_container = self.form_container.bind(parent=self) def own_evaluate_parameters(self): return dict(query=self) def get_advanced_query_param(self): return '-' + path_join(self.iommi_path, 'query') def parse_query_string(self, query_string: str) -> Q: assert self._is_bound query_string = query_string.strip() if not query_string: return Q() parser = self._create_grammar() try: tokens = parser.parseString(query_string, parseAll=True) except ParseException as e: raise QueryException('Invalid syntax for query') return self._compile(tokens) def _compile(self, tokens) -> Q: items = [] for token in tokens: if isinstance(token, ParseResults): items.append(self._compile(token)) elif isinstance(token, Q): items.append(token) elif token in ('and', 'or'): items.append(token) return self._rpn_to_q(self._tokens_to_rpn(items)) @staticmethod def _rpn_to_q(tokens): stack = [] for each in tokens: if isinstance(each, Q): stack.append(each) else: op = each # infix right hand operator is on the top of the stack right, left = stack.pop(), stack.pop() stack.append(left & right if op == 'and' else left | right) assert len(stack) == 1 return stack[0] @staticmethod def _tokens_to_rpn(tokens): # Convert a infix sequence of Q objects and 'and/or' operators using # dijkstra shunting yard algorithm into RPN if len(tokens) == 1: return tokens result_q, stack = [], [] for token in tokens: assert token is not None if isinstance(token, Q): result_q.append(token) elif token in PRECEDENCE: p1 = PRECEDENCE[token] while stack: t2, p2 = stack[-1] if p1 <= p2: stack.pop() result_q.append(t2) else: # pragma: no cover break # pragma: no mutate stack.append((token, PRECEDENCE[token])) while stack: result_q.append(stack.pop()[0]) return result_q def _create_grammar(self): """ Pyparsing implementation of a where clause grammar based on http://pyparsing.wikispaces.com/file/view/simpleSQL.py The query language is a series of statements separated by AND or OR operators and parentheses can be used to group/provide precedence. A statement is a combination of three strings "<filter> <operator> <value>" or "<filter> <operator> <filter>". A value can be a string, integer or a real(floating) number or a (ISO YYYY-MM-DD) date. An operator must be one of "= != < > >= <= !:" and are translated into django __lte or equivalent suffixes. See self.as_q Example something < 10 AND other >= 2015-01-01 AND (foo < 1 OR bar > 1) """ quoted_string_excluding_quotes = QuotedString( '"', escChar='\\').setParseAction(lambda token: StringValue(token[0])) and_ = Keyword('and', caseless=True) or_ = Keyword('or', caseless=True) binary_op = oneOf('=> =< = < > >= <= : != !:', caseless=True).setResultsName('operator') # define query tokens identifier = Word(alphas, alphanums + '_$-.').setName('identifier') raw_value_chars = alphanums + '_$-+/$%*;?@[]\\^`{}|~.' raw_value = Word(raw_value_chars, raw_value_chars).setName('raw_value') value_string = quoted_string_excluding_quotes | raw_value # Define a where expression where_expression = Forward() binary_operator_statement = (identifier + binary_op + value_string).setParseAction( self._binary_op_to_q) unary_operator_statement = (identifier | (Char('!') + identifier)).setParseAction( self._unary_op_to_q) free_text_statement = quotedString.copy().setParseAction( self._freetext_to_q) operator_statement = binary_operator_statement | free_text_statement | unary_operator_statement where_condition = Group(operator_statement | ('(' + where_expression + ')')) where_expression << where_condition + ZeroOrMore( (and_ | or_) + where_expression) # define the full grammar query_statement = Forward() query_statement << Group(where_expression).setResultsName("where") return query_statement def _unary_op_to_q(self, token): if len(token) == 1: (filter_name, ) = token value = 'true' else: (op, filter_name) = token value = 'false' if op != '!': # pragma: no cover. You can't actually get here because you'll get a syntax error earlier raise QueryException( f'Unknown unary filter operator "{op}", available operators: !' ) filter = self.filters.get(filter_name.lower()) if filter: if not filter.unary: raise QueryException( f'"{filter_name}" is not a unary filter, you must use it like "{filter_name}=something"' ) result = filter.value_to_q(filter=filter, op='=', value_string_or_f=value) return result raise QueryException( f'Unknown unary filter "{filter_name}", available filters: {", ".join(list(keys(self.filters)))}' ) def _binary_op_to_q(self, token): """ Convert a parsed token of filter_name OPERATOR filter_name into a Q object """ assert self._is_bound filter_name, op, value_string_or_filter_name = token if filter_name.endswith('.pk'): filter = self.filters.get(filter_name.lower()[:-len('.pk')]) if op != '=': raise QueryException( 'Only = is supported for primary key lookup') try: pk = int(value_string_or_filter_name) except ValueError: raise QueryException( f'Could not interpret {value_string_or_filter_name} as an integer' ) return Q(**{f'{filter.attr}__pk': pk}) filter = self.filters.get(filter_name.lower()) if filter: if isinstance(value_string_or_filter_name, str) and not isinstance( value_string_or_filter_name, StringValue ) and value_string_or_filter_name.lower() in self.filters: value_string_or_f = F( self.filters[value_string_or_filter_name.lower()].attr) else: value_string_or_f = value_string_or_filter_name try: result = filter.value_to_q(filter=filter, op=op, value_string_or_f=value_string_or_f) except ValidationError as e: raise QueryException(f'{e.message}') if result is None: raise QueryException( f'Unknown value "{value_string_or_f}" for filter "{filter._name}"' ) return result raise QueryException( f'Unknown filter "{filter_name}", available filters: {list(keys(self.filters))}' ) def _freetext_to_q(self, token): if all(not v.freetext for v in values(self.filters)): raise QueryException('There are no freetext filters available') assert len(token) == 1 token = token[0].strip('"') return reduce(operator.or_, [ Q( **{ filter.attr + '__' + filter.query_operator_to_q_operator(':'): token }) for filter in values(self.filters) if filter.freetext ]) def get_query_string(self): """ Based on the data in the request, return the equivalent query string that you can use with parse_query_string() to create a query set. """ form = self.form request = self.get_request() if request is None: return '' if request_data(request).get(self.get_advanced_query_param(), '').strip(): return request_data(request).get(self.get_advanced_query_param()) elif form.is_valid(): def expr(field, is_list, value): if is_list: return '(' + ' OR '.join([ expr(field, is_list=False, value=x) for x in field.value ]) + ')' return build_query_expression(field=field, filter=self.filters[field._name], value=value) result = [ expr(field, field.is_list, field.value) for field in values(form.fields) if field._name != FREETEXT_SEARCH_NAME and field.value not in ( None, '', []) ] if FREETEXT_SEARCH_NAME in form.fields: freetext = form.fields[FREETEXT_SEARCH_NAME].value if freetext: result.append('(%s)' % ' or '.join([ f'{filter._name}:{to_string_surrounded_by_quote(freetext)}' for filter in values(self.filters) if filter.freetext ])) return ' and '.join(result) else: return '' def get_q(self): """ Create a query set based on the data in the request. """ try: return self.parse_query_string(self.get_query_string()) except QueryException as e: self.query_error = str(e) raise @classmethod @dispatch( filters=EMPTY, ) def filters_from_model(cls, filters, **kwargs): return create_members_from_model( member_class=cls.get_meta().member_class, member_params_by_member_name=filters, **kwargs) @classmethod @dispatch( filters=EMPTY, ) def _from_model(cls, *, rows=None, model=None, filters, include=None, exclude=None): assert rows is None or isinstance(rows, QuerySet), \ 'auto__rows needs to be a QuerySet for filter generation to work. ' \ 'If it needs to be a lambda, provide a model with auto__model for filter generation, ' \ 'and pass the lambda as rows.' model, rows = model_and_rows(model, rows) assert model is not None or rows is not None, "auto__model or auto__rows must be specified" filters = cls.filters_from_model(model=model, include=include, exclude=exclude, filters=filters) return model, rows, filters
class QueryAutoConfig(AutoConfig): rows = Refinable()
class Filter(Part): """ Class that describes a filter that you can search for. See :doc:`Query` for more complete examples. """ attr = EvaluatedRefinable() field: Namespace = Refinable() query_operator_for_field: str = EvaluatedRefinable() freetext = EvaluatedRefinable() model: Type[Model] = Refinable( ) # model is evaluated, but in a special way so gets no EvaluatedRefinable type model_field = Refinable() model_field_name = Refinable() choices = EvaluatedRefinable() search_fields = Refinable() unary = Refinable() @reinvokable @dispatch( query_operator_for_field='=', attr=MISSING, search_fields=MISSING, field__required=False, field__include=lambda query, field, **_: not query.filters.get( field._name).freetext, ) def __init__(self, **kwargs): """ Parameters with the prefix `field__` will be passed along downstream to the `Field` instance if applicable. This can be used to tweak the basic style interface. :param field__include: set to `True` to display a GUI element for this filter in the basic style interface. :param field__call_target: the factory to create a `Field` for the basic GUI, for example `Field.choice`. Default: `Field` """ super(Filter, self).__init__(**kwargs) def on_bind(self) -> None: if self.attr is MISSING: self.attr = self._name # Not strict evaluate on purpose self.model = evaluate(self.model, **self.iommi_evaluate_parameters()) if self.model and self.include and self.attr: try: self.search_fields = get_search_fields(model=self.model) except NoRegisteredSearchFieldException: self.search_fields = ['pk'] if iommi_debug_on(): print( f'Warning: falling back to primary key as lookup and sorting on {self._name}. \nTo get rid of this warning and get a nicer lookup and sorting use register_search_fields for model {self.model}' ) def own_evaluate_parameters(self): return dict(filter=self) @staticmethod @refinable def query_operator_to_q_operator(op: str) -> str: return Q_OPERATOR_BY_QUERY_OPERATOR[op] @staticmethod @refinable def parse(string_value, **_): return string_value @staticmethod @refinable def value_to_q(filter, op, value_string_or_f) -> Q: if filter.attr is None: return Q() negated = False if op in ('!=', '!:'): negated = True op = op[1:] is_str = isinstance(value_string_or_f, str) if is_str and value_string_or_f.lower() == 'null': r = Q(**{filter.attr: None}) else: if is_str: value_string_or_f = filter.parse( filter=filter, string_value=value_string_or_f, op=op) r = Q( **{ filter.attr + '__' + filter.query_operator_to_q_operator(op): value_string_or_f }) if negated: return ~r else: return r @classmethod def from_model(cls, model, model_field_name=None, model_field=None, **kwargs): return member_from_model( cls=cls, model=model, factory_lookup=_filter_factory_by_django_field_type, model_field_name=model_field_name, model_field=model_field, defaults_factory=lambda model_field: {}, **kwargs) @classmethod @class_shortcut( field__call_target__attribute='text', query_operator_for_field=':', ) def text(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( query_operator_to_q_operator= case_sensitive_query_operator_to_q_operator, ) def case_sensitive(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='choice', ) def choice(cls, call_target=None, **kwargs): """ Field that has one value out of a set. :type choices: list """ setdefaults_path(kwargs, dict(field__choices=kwargs.get('choices'), )) return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='multi_choice', ) def multi_choice(cls, call_target=None, **kwargs): """ Field that has one value out of a set. :type choices: list """ setdefaults_path(kwargs, dict(field__choices=kwargs.get('choices'), )) return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='choice_queryset', query_operator_to_q_operator=lambda op: 'exact', value_to_q=choice_queryset_value_to_q, ) def choice_queryset(cls, choices: QuerySet, call_target=None, **kwargs): """ Field that has one value out of a set. """ if 'model' not in kwargs: assert isinstance( choices, QuerySet ), 'The convenience feature to automatically get the parameter model set only works for QuerySet instances' kwargs['model'] = choices.model setdefaults_path( kwargs, dict( field__choices=choices, field__model=kwargs['model'], choices=choices, )) return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute="choice_queryset", field__call_target__attribute='multi_choice_queryset', ) def multi_choice_queryset(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='boolean', parse=bool_parse, unary=True, ) def boolean(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='boolean_tristate', parse=boolean_tristate__parse, unary=True, ) def boolean_tristate(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='integer', parse=int_parse, ) def integer(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='float', parse=float_parse, ) def float(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='url', ) def url(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='time', parse=time_parse, ) def time(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='datetime', parse=datetime_parse, ) def datetime(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='date', parse=date_parse, ) def date(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='email', ) def email(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='decimal', ) def decimal(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( field__call_target__attribute='file', ) def file(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='choice_queryset', field__call_target__attribute='foreign_key', ) def foreign_key(cls, model_field, call_target, **kwargs): setdefaults_path( kwargs, choices=model_field.foreign_related_fields[0].model.objects.all(), ) return call_target(model_field=model_field, **kwargs) @classmethod @class_shortcut( call_target__attribute='multi_choice_queryset', field__call_target__attribute='many_to_many', ) def many_to_many(cls, call_target, model_field, **kwargs): setdefaults_path( kwargs, choices=model_field.remote_field.model.objects.all(), extra__django_related_field=True, ) kwargs['model'] = model_field.remote_field.model return call_target(model_field=model_field, **kwargs)
class Admin(Page): class Meta: iommi_style = 'bootstrap' table_class = Table form_class = Form apps__auth_user__include = True apps__auth_group__include = True parts__messages = Messages() parts__list_auth_user = dict( auto__include=['username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active', 'is_superuser'], columns=dict( username__filter__freetext=True, email__filter__freetext=True, first_name__filter__freetext=True, last_name__filter__freetext=True, is_staff__filter__include=True, is_active__filter__include=True, is_superuser__filter__include=True, ), ) table_class: Type[Table] = Refinable() form_class: Type[Form] = Refinable() apps: Namespace = Refinable() # Global configuration on apps level menu = Menu( sub_menu=dict( root=MenuItem(url=lambda admin, **_: reverse(admin.__class__.all_models), display_name=gettext('iommi administration')), change_password=MenuItem(url=lambda **_: reverse(Auth.change_password), display_name=gettext('Change password')), logout=MenuItem(url=lambda **_: reverse(Auth.logout), display_name=gettext('Logout')), ), ) @read_config @reinvokable @dispatch( apps=EMPTY, parts=EMPTY, ) def __init__(self, parts, apps, **kwargs): # Validate apps params for k in apps.keys(): assert k in joined_app_name_and_model, f'{k} is not a valid app/model key.\n\nValid keys:\n ' + '\n '.join(joined_app_name_and_model) def should_throw_away(k, v): if isinstance(v, Namespace) and 'call_target' in v: return False if k == 'all_models': return True prefix_blacklist = [ 'list_', 'delete_', 'create_', 'edit_', ] for prefix in prefix_blacklist: if k.startswith(prefix): return True return False parts = { # Arguments that are not for us needs to be thrown on the ground k: None if should_throw_away(k, v) else v for k, v in items(parts) } super(Admin, self).__init__(parts=parts, apps=apps, **kwargs) @staticmethod def has_permission(request, operation, model=None, instance=None): return request.user.is_staff def own_evaluate_parameters(self): return dict(admin=self, **super(Admin, self).own_evaluate_parameters()) @classmethod @class_shortcut( table=EMPTY, table__call_target__attribute='div', ) @require_login def all_models(cls, request, table, call_target=None, **kwargs): if not cls.has_permission(request, operation='all_models'): raise Http404() def rows(admin, **_): for app_name, models in items(django_apps.all_models): has_yielded_header = False for model_name, model in sorted(items(models), key=lambda x: x[1]._meta.verbose_name_plural): if not admin.apps.get(f'{app_name}_{model_name}', {}).get('include', False): continue if not has_yielded_header: yield Struct( name=app_verbose_name_by_label[app_name], verbose_app_name=app_verbose_name_by_label[app_name], url=None, format=lambda row, table, **_: Header(row.name, _name='invalid_name').bind(parent=table).__html__() ) has_yielded_header = True yield Struct( verbose_app_name=app_verbose_name_by_label[app_name], app_name=app_name, name=model._meta.verbose_name_plural.capitalize(), url='%s/%s/' % (app_name, model_name), format=lambda row, **_: row.name, ) table = setdefaults_path( Namespace(), table, title=gettext('All models'), call_target__cls=cls.get_meta().table_class, sortable=False, rows=rows, header__template=None, page_size=None, columns__name=dict( cell__url=lambda row, **_: row.url, display_name='', cell__format=lambda row, **kwargs: row.format(row=row, **kwargs), ), ) return call_target( parts__all_models=table, **kwargs ) @classmethod @class_shortcut( table=EMPTY, ) @require_login def list(cls, request, app_name, model_name, table, call_target=None, **kwargs): model = django_apps.all_models[app_name][model_name] if not cls.has_permission(request, operation='list', model=model): raise Http404() table = setdefaults_path( Namespace(), table, call_target__cls=cls.get_meta().table_class, auto__model=model, columns=dict( select__include=True, edit=dict( call_target__attribute='edit', after=0, cell__url=lambda row, **_: '%s/edit/' % row.pk, ), delete=dict( call_target__attribute='delete', after=LAST, cell__url=lambda row, **_: '%s/delete/' % row.pk, ), ), actions=dict( create=dict( display_name=gettext('Create %(model_name)s') % dict(model_name=model._meta.verbose_name), attrs__href='create/', ), ), query_from_indexes=True, bulk__actions__delete__include=True, ) return call_target( parts__header__children__link__attrs__href='../..', **{f'parts__list_{app_name}_{model_name}': table}, **kwargs, ) @classmethod @class_shortcut( form=EMPTY, ) @require_login def crud(cls, request, operation, form, app_name, model_name, pk=None, call_target=None, **kwargs): model = django_apps.all_models[app_name][model_name] instance = model.objects.get(pk=pk) if pk is not None else None if not cls.has_permission(request, operation=operation, model=model, instance=instance): raise Http404() def on_save(form, instance, **_): message = f'{form.model._meta.verbose_name.capitalize()} {instance} was ' + ('created' if form.extra.is_create else 'updated') messages.add_message(request, messages.INFO, message, fail_silently=True) def on_delete(form, instance, **_): message = f'{form.model._meta.verbose_name.capitalize()} {instance} was deleted' messages.add_message(request, messages.INFO, message, fail_silently=True) form = setdefaults_path( Namespace(), form, call_target__cls=cls.get_meta().form_class, auto__instance=instance, auto__model=model, call_target__attribute=operation, extra__on_save=on_save, extra__on_delete=on_delete, ) return call_target( **{f'parts__{operation}_{app_name}_{model_name}': form}, **kwargs, ) @classmethod @class_shortcut( call_target__attribute='crud', operation='create', parts__header__children__link__attrs__href='../../..', ) def create(cls, request, call_target, **kwargs): return call_target(request=request, **kwargs) @classmethod @class_shortcut( call_target__attribute='crud', operation='edit', parts__header__children__link__attrs__href='../../../..', ) def edit(cls, request, call_target, **kwargs): return call_target(request=request, **kwargs) @classmethod @class_shortcut( call_target__attribute='crud', operation='delete', parts__header__children__link__attrs__href='../../../..', ) def delete(cls, request, call_target, **kwargs): return call_target(request=request, **kwargs) @classmethod def urls(cls): return Struct( urlpatterns=[ path('', cls.all_models), path('<app_name>/<model_name>/', cls.list), path('<app_name>/<model_name>/create/', cls.create), path('<app_name>/<model_name>/<int:pk>/edit/', cls.edit), path('<app_name>/<model_name>/<int:pk>/delete/', cls.delete), ] + Auth.urls().urlpatterns )
class MenuItem(MenuBase): """ Class that is used for the clickable menu items in a menu. See :doc:`Menu` for more complete examples. """ display_name: str = EvaluatedRefinable() url: str = Refinable( ) # attrs is evaluated, but in a special way so gets no EvaluatedRefinable type regex: str = EvaluatedRefinable() group: str = EvaluatedRefinable() a = Refinable() active_class = Refinable() @reinvokable @dispatch( display_name=lambda menu_item, **_: capitalize(menu_item.iommi_name()). replace('_', ' '), regex=lambda menu_item, **_: '^' + menu_item.url if menu_item.url else None, url=lambda menu_item, **_: '/' + path_join( getattr(menu_item.iommi_parent(), 'url', None), menu_item.iommi_name()) + '/', a=EMPTY, ) def __init__(self, **kwargs): super(MenuItem, self).__init__(**kwargs) def on_bind(self): super(MenuItem, self).on_bind() self.url = evaluate_strict(self.url, **self.iommi_evaluate_parameters()) # If this is a section header, and all sub-parts are hidden, hide myself if not self.url and self.sub_menu is not None and not self.sub_menu: self.include = False def own_evaluate_parameters(self): return dict(menu_item=self) def __repr__(self): r = f'{self._name} -> {self.url}' if self.sub_menu: for items in values(self.sub_menu): r += ''.join([f'\n {x}' for x in repr(items).split('\n')]) return r def __html__(self, *, render=None): a = setdefaults_path( Namespace(), self.a, children__text=self.display_name, attrs__href=self.url, _name='a', ) if self._active: setdefaults_path( a, attrs__class={self.active_class: True}, ) if self.url is None and a.tag == 'a': a.tag = None fragment = Fragment( children__a=a, tag=self.tag, template=self.template, attrs=self.attrs, _name='fragment', ) fragment = fragment.bind(parent=self) # need to do this here because otherwise the sub menu will get get double bind for name, item in items(self.sub_menu): assert name not in fragment.children fragment.children[name] = item return fragment.__html__()
class Menu(MenuBase): """ Class that describes menus. Example: .. code:: python menu = Menu( sub_menu=dict( root=MenuItem(url='/'), albums=MenuItem(url='/albums/'), # url defaults to /<name>/ so we # don't need to write /musicians/ here musicians=MenuItem(), ), ) """ items_container = Refinable() @reinvokable @dispatch( sort=False, items_container=EMPTY, ) def __init__(self, **kwargs): super(Menu, self).__init__(**kwargs) self.fragment = None def __html__(self, *, render=None): return self.fragment.__html__() def own_evaluate_parameters(self): return dict(menu=self) def on_bind(self): super(Menu, self).on_bind() self.fragment = Fragment( _name=self._name, tag=self.tag, template=self.template, attrs=self.attrs, children__items_container=Fragment(**self.items_container, )).bind( parent=self) # need to do this here because otherwise the sub menu will get get double bind items_container = self.fragment.children.items_container for name, item in items(self.sub_menu): assert name not in items_container.children items_container.children[name] = item self.set_active(current_path=self.get_request().path) def validate(self): # verify there is no ambiguity for the MenuItems paths = defaultdict(list) def _validate(item): for sub_item in values(item.sub_menu): if sub_item.url is None or '://' in sub_item.url or sub_item.url.startswith( '#'): continue _validate(sub_item) path = urlparse(sub_item.url).path paths[path].append(sub_item.iommi_path) _validate(self) ambiguous = {k: v for k, v in items(paths) if len(v) > 1} return ambiguous def set_active(self, current_path: str): current = None current_parts_matching = 0 path_parts = PurePosixPath(current_path).parts def _set_active(item): nonlocal current_parts_matching nonlocal current for sub_item in values(item.sub_menu): _set_active(sub_item) if sub_item.url is None or '://' in sub_item.url: continue parsed_url = urlparse(sub_item.url).path if current_path.startswith(parsed_url): parts = PurePosixPath(unquote(parsed_url)).parts matching_parts = 0 for item in range(min(len(parts), len(path_parts))): if parts[item] is path_parts[item]: matching_parts += 1 if matching_parts > current_parts_matching: current = sub_item current_parts_matching = matching_parts _set_active(self) if current: current._active = True
class PagePart(RefinableObject): name: str = Refinable() include: bool = Refinable() after: Union[int, str] = Refinable() default_child = Refinable() extra: Namespace = Refinable() extra_evaluated: Namespace = Refinable() style: str = Refinable() parent = None _is_bound = False @dispatch( extra=EMPTY, extra_evaluated=EMPTY, include=True, name=None, ) def __init__(self, **kwargs): super(PagePart, self).__init__(**kwargs) @dispatch( context=EMPTY, render=EMPTY, ) def __html__(self, *, context=None, render=None): assert False, 'Not implemented' def __str__(self): assert self._is_bound return self.__html__() @dispatch def render_to_response(self, **kwargs): request = self.request() req_data = request_data(request) if request.method == 'GET': dispatch_prefix = DISPATCH_PATH_SEPARATOR dispatcher = perform_ajax_dispatch dispatch_error = 'Invalid endpoint path' def dispatch_response_handler(r): return HttpResponse(json.dumps(r), content_type='application/json') elif request.method == 'POST': dispatch_prefix = '-' dispatcher = perform_post_dispatch dispatch_error = 'Invalid post path' def dispatch_response_handler(r): return r else: assert False # This has already been checked in request_data() dispatch_commands = {key: value for key, value in req_data.items() if key.startswith(dispatch_prefix)} assert len(dispatch_commands) in (0, 1), 'You can only have one or no dispatch commands' if dispatch_commands: dispatch_target, value = next(iter(dispatch_commands.items())) try: result = dispatcher(root=self, path=dispatch_target, value=value) except InvalidEndpointPathException: if settings.DEBUG: raise result = dict(error=dispatch_error) if result is not None: return dispatch_response_handler(result) return HttpResponse(render_root(part=self, **kwargs)) def bind(self, *, parent=None, request=None): assert parent is None or parent._is_bound assert not self._is_bound if parent is None: self._request = request if self.name is None: self.name = 'root' if self.default_child is None: self.default_child = True if hasattr(self, '_no_copy_on_bind'): result = self else: result = copy.copy(self) result._declared = self del self # to prevent mistakes when changing the code below result.parent = parent result._is_bound = True apply_style(result) result.on_bind() if len(result.children()) == 1: for the_only_part in result.children().values(): if the_only_part.default_child is None: the_only_part.default_child = True return result def on_bind(self) -> None: pass def children(self): assert self._is_bound return Struct() def request(self): if self.parent is None: return self._request else: return self.parent.request() def dunder_path(self) -> str: assert self._is_bound if self.parent is not None: return path_join(self.parent.dunder_path(), self.name, separator='__') else: assert self.name, f'{self} is missing a name, but it was asked about its path' return '' def path(self) -> str: assert self._is_bound if self.default_child: if self.parent is not None: return self.parent.path() else: return '' if self.parent is not None: return path_join(self.parent.path(), self.name) else: assert self.name, f'{self} is missing a name, but it was asked about its path' return self.name def endpoint_path(self): return DISPATCH_PREFIX + self.path() def evaluate_attribute_kwargs(self): return {**self._evaluate_attribute_kwargs(), **(self.parent.evaluate_attribute_kwargs() if self.parent is not None else {})} def _evaluate_attribute_kwargs(self): return {} def _evaluate_attribute(self, key, strict=True): evaluate_member(self, key, **self.evaluate_attribute_kwargs(), strict=strict) def _evaluate_include(self): self._evaluate_attribute('include')
class Page(PagePart): member_class: Type[Fragment] = Refinable() class Meta: member_class = Fragment @dispatch( parts=EMPTY, ) def __init__(self, *, _parts_dict: Dict[str, PartType] = None, parts: dict, **kwargs): super(Page, self).__init__(**kwargs) self.parts = { } # This is just so that the repr can survive if it gets triggered before parts is set properly # First we have to up sample parts that aren't PagePart into Fragment def as_fragment_if_needed(k, v): if not isinstance(v, PagePart): return Fragment(v, name=k) else: return v _parts_dict = { k: as_fragment_if_needed(k, v) for k, v in _parts_dict.items() } parts = Namespace( {k: as_fragment_if_needed(k, v) for k, v in parts.items()}) self._columns_unapplied_data = {} self.declared_parts: Dict[str, PartType] = collect_members( items=parts, items_dict=_parts_dict, cls=self.get_meta().member_class, unapplied_config=self._columns_unapplied_data) def on_bind(self) -> None: bind_members(self, name='parts', default_child=True) def __repr__(self): return f'<Page with parts: {list(self.parts.keys())}>' def children(self): assert self._is_bound return self.parts def _evaluate_attribute_kwargs(self): return dict(page=self) @dispatch(context=EMPTY, render=lambda rendered: format_html('{}' * len(rendered), *rendered.values())) def __html__(self, *, context=None, render=None): rendered = {} for part in self.parts.values(): assert part.name not in context rendered[part.name] = as_html(part=part, context=context) return render(rendered)