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 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"`. An endpoint can return an HttpResponse directly, a `Part` which is rendered for you, and everything else we try to dump to json for you. """ func: Callable = Refinable() include: bool = EvaluatedRefinable() @dispatch( 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 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!'), endpoint__echo__func=lambda value, **_: value, ) 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 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 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 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 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 Fragment(Part): """ `Fragment` is a class used to build small HTML fragments that plug into iommis structure. .. code:: python h1 = Fragment(children__text='Tony', tag='h1') It's easiest to use via the html builder: .. code:: python h1 = html.h1('Tony') Fragments are useful because attrs, template and tag are evaluated, so if you have a `Page` with a fragment in it you can configure it later: .. code:: python class MyPage(Page): header = html.h1( 'Hi!', attrs__class__staff= lambda request, **_: request.user.is_staff, ) Rendering a `MyPage` will result in a `<h1>`, but if you do `MyPage(parts__header__tag='h2')` it will be rendered with a `<h2>`. """ attrs: Attrs = Refinable( ) # attrs is evaluated, but in a special way so gets no EvaluatedRefinable type tag = EvaluatedRefinable() template: Union[str, Template] = EvaluatedRefinable() @reinvokable @dispatch( tag=None, children=EMPTY, attrs__class=EMPTY, attrs__style=EMPTY, ) def __init__(self, text=None, *, children: Optional[Dict[str, PartType]] = None, **kwargs): super(Fragment, self).__init__(**kwargs) if text is not None: setdefaults_path( children, text=text, ) collect_members(self, name='children', items=children, cls=Fragment, unknown_types_fall_through=True) def render_text_or_children(self, context): assert not isinstance(context, RequestContext) return format_html( '{}' * len(self.children), *[as_html(part=x, context=context) for x in values(self.children)]) def __repr__(self): return f'<{self.__class__.__name__} tag:{self.tag} attrs:{dict(self.attrs) if self.attrs else None!r}>' def on_bind(self) -> None: bind_members(self, name='children', unknown_types_fall_through=True) # Fragment children are special and they can be raw str/int etc but # also callables. We need to evaluate them! children = evaluate_strict_container( self.children, **self.iommi_evaluate_parameters()) self.children.update(children) self._bound_members.children._bound_members.update(children) @dispatch( render=fragment__render, ) def __html__(self, *, render=None): assert self._is_bound return render( fragment=self, context={ **self.get_context(), **self.iommi_evaluate_parameters() }, ) def own_evaluate_parameters(self): return dict(fragment=self)
class Page(Part): """ A page is used to compose iommi parts into a bigger whole. See the `howto <https://docs.iommi.rocks/en/latest/howto.html#parts-pages>`_ for example usages. """ title: str = EvaluatedRefinable() member_class: Type[Fragment] = Refinable() context = Refinable( ) # context is evaluated, but in a special way so gets no EvaluatedRefinable type class Meta: member_class = Fragment @reinvokable @dispatch( parts=EMPTY, context=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 Part into Fragment def as_fragment_if_needed(k, v): if v is None: return None if not isinstance(v, (dict, Traversable)): return Fragment(children__text=v, _name=k) else: return v _parts_dict = { k: as_fragment_if_needed(k, v) for k, v in items(_parts_dict) } parts = Namespace( {k: as_fragment_if_needed(k, v) for k, v in items(parts)}) collect_members(self, name='parts', items=parts, items_dict=_parts_dict, cls=self.get_meta().member_class) def on_bind(self) -> None: bind_members(self, name='parts') if self.context and self.iommi_parent() != None: assert False, 'The context property is only valid on the root page' def own_evaluate_parameters(self): return dict(page=self) @dispatch(render=lambda rendered: format_html('{}' * len(rendered), *values(rendered))) def __html__(self, *, render=None): self.context = evaluate_strict_container( self.context or {}, **self.iommi_evaluate_parameters()) rendered = { name: as_html(request=self.get_request(), part=part, context=self.iommi_evaluate_parameters()) for name, part in items(self.parts) } return render(rendered) def as_view(self): return build_as_view_wrapper(self)
class Part(Traversable): """ `Part` is the base class for parts of a page that can be rendered as html, and can respond to ajax and post. See the `howto <https://docs.iommi.rocks/en/latest/howto.html#parts-pages>`_ for example usages. """ include: bool = Refinable( ) # This is evaluated, but first and in a special way after: Union[int, str] = EvaluatedRefinable() extra: Dict[str, Any] = Refinable() extra_evaluated: Dict[str, Any] = Refinable( ) # not EvaluatedRefinable because this is an evaluated container so is special endpoints: Namespace = Refinable() # Only the assets used by this part assets: Namespace = Refinable() @reinvokable @dispatch( extra=EMPTY, include=True, ) def __init__(self, *, endpoints: Dict[str, Any] = None, assets: Dict[str, Any] = None, include, **kwargs): from iommi.asset import Asset super(Part, self).__init__(include=include, **kwargs) collect_members(self, name='endpoints', items=endpoints, cls=Endpoint) collect_members(self, name='assets', items=assets, cls=Asset) if iommi_debug_on(): import inspect self._instantiated_at_frame = inspect.currentframe().f_back @dispatch( render=EMPTY, ) @abstractmethod def __html__(self, *, render=None): assert False, 'Not implemented' # pragma: no cover, no mutate def __str__(self): assert self._is_bound, NOT_BOUND_MESSAGE return self.__html__() def bind(self, *, parent=None, request=None): result = super(Part, self).bind(parent=parent, request=request) if result is None: return None del self bind_members(result, name='endpoints') bind_members(result, name='assets', lazy=False) result.iommi_root()._iommi_collected_assets.update(result.assets) return result @dispatch def render_to_response(self, **kwargs): request = self.get_request() req_data = request_data(request) def dispatch_response_handler(r): if isinstance(r, HttpResponseBase): return r elif isinstance(r, Part): if not r._is_bound: r = r.bind(request=request) return HttpResponse(render_root(part=r, **kwargs)) else: return HttpResponse(json.dumps(r), content_type='application/json') if request.method == 'GET': dispatch_prefix = DISPATCH_PATH_SEPARATOR dispatcher = perform_ajax_dispatch dispatch_error = 'Invalid endpoint path' elif request.method == 'POST': dispatch_prefix = '-' dispatcher = perform_post_dispatch dispatch_error = 'Invalid post path' else: # pragma: no cover assert False # This has already been checked in request_data() dispatch_commands = { key: value for key, value in items(req_data) 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) else: if request.method == 'POST': assert False, 'This request was a POST, but there was no dispatch command present.' response = HttpResponse(render_root(part=self, **kwargs)) response.iommi_part = self return response def iommi_collected_assets(self): return sort_after(self.iommi_root()._iommi_collected_assets)
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(attrs__value='Button title!') # A submit button Action.submit(display_name='Do this') # The primary submit button on a form, unnecessary # most of the time as a form includes a submit # button by default. Action.primary() # A button styled as primary but not using # the submit html element, but the button # element. Action.primary(call_target__attribute='button') """ 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 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', tag='input', attrs__type='submit', 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 Foo(Traversable): bar = EvaluatedRefinable()
class Field(Part): """ Class that describes a field, i.e. what input controls to render, the label, etc. See :doc:`Form` for more complete examples. The life cycle of the data is: 1. `raw_data`/`raw_data_list`: will be set if the corresponding key is present in the HTTP request 2. `parsed_data`: set if parsing is successful, which only happens if the previous step succeeded 3. `value`: set if validation is successful, which only happens if the previous step succeeded """ attr: str = EvaluatedRefinable() display_name: str = EvaluatedRefinable() # raw_data/raw_data contains the strings grabbed directly from the request data # It is useful that they are evaluated for example when doing file upload. In that case the data is on request.FILES, not request.POST so we can use this to grab it from there raw_data: str = Refinable() # raw_data is evaluated, but in a special way raw_data_list: List[str] = Refinable() # raw_data_list is evaluated, but in a special way parse_empty_string_as_none: bool = EvaluatedRefinable() # parsed_data/parsed_data contains data that has been interpreted, but not checked for validity or access control parsed_data: Any = Refinable() # parsed_data is evaluated, but in a special way so gets no EvaluatedRefinable type initial: Any = Refinable() # initial is evaluated, but in a special way so gets no EvaluatedRefinable type template: Union[str, Template] = EvaluatedRefinable() attrs: Attrs = Refinable() # attrs is evaluated, but in a special way so gets no EvaluatedRefinable type required: bool = EvaluatedRefinable() input: Fragment = Refinable() label: Fragment = Refinable() non_editable_input: Fragment = Refinable() is_list: bool = EvaluatedRefinable() is_boolean: bool = 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() editable: bool = EvaluatedRefinable() strip_input: bool = EvaluatedRefinable() choices: Callable[..., List[Any]] = Refinable() # choices is evaluated, but in a special way so gets no EvaluatedRefinable type choice_to_option: Callable[..., Tuple[Any, str, str, bool]] = Refinable() search_fields = Refinable() errors: Errors = Refinable() empty_label: str = EvaluatedRefinable() empty_choice_tuple: Tuple[Any, str, str, bool] = EvaluatedRefinable() @reinvokable @dispatch( attr=MISSING, display_name=MISSING, attrs__class=EMPTY, attrs__style=EMPTY, parse_empty_string_as_none=True, required=True, is_list=False, is_boolean=False, editable=True, strip_input=True, endpoints__config__func=default_endpoints__config, endpoints__validate__func=default_endpoints__validate, errors=EMPTY, label__call_target=Fragment, label__attrs__for=default_input_id, input__call_target=Fragment, input__attrs__id=default_input_id, input__attrs__name=lambda field, **_: field.iommi_path, input__extra__placeholder='', non_editable_input__call_target=Fragment, non_editable_input__attrs__type=None, initial=MISSING, ) def __init__(self, **kwargs): """ Note that, in addition to the parameters with the defined behavior below, you can pass in any keyword argument you need yourself, including callables that conform to the protocol, and they will be added and evaluated as members. All these parameters can be callables, and if they are, will be evaluated with the keyword arguments form and field. The only exceptions are `is_valid` (which gets `form`, `field` and `parsed_data`), `render_value` (which takes `form`, `field` and `value`) and `parse` (which gets `form`, `field`, `string_value`). Example of using a lambda to specify a value: .. code:: python Field(attrs__id=lambda form, field: 'my_id_%s' % field._name) :param after: Set the order of columns, see the `howto <https://docs.iommi.rocks/en/latest/howto.html#how-do-i-change-the-order-of-the-fields>`_ for an example. :param is_valid: validation function. Should return a tuple of `(bool, reason_for_failure_if_bool_is_false)` or raise ValidationError. Default: `lambda form, field, parsed_data: (True, '')` :param parse: parse function. Default just returns the string input unchanged: `lambda form, field, string_value: string_value` :param initial: initial value of the field :param attr: the attribute path to apply or get the data from. For example using `foo__bar__baz` will result in `your_instance.foo.bar.baz` will be set by the `apply()` function. Defaults to same as name :param attrs: a dict containing any custom html attributes to be sent to the `input__template`. :param display_name: the text in the HTML label tag. Default: `capitalize(name).replace('_', ' ')` :param template: django template filename for the entire row. Normally you shouldn't need to override on this level. Prefer overriding `input__template`, `label__template` or `error__template` as needed. :param template_string: You can inline a template string here if it's more convenient than creating a file. Default: `None` :param input__template: django template filename for the template for just the input control. :param label__template: django template filename for the template for just the label tab. :param errors__template: django template filename for the template for just the errors output. Default: `'iommi/form/errors.html'` :param required: if the field is a required field. Default: `True` :param help_text: The help text will be grabbed from the django model if specified and available. :param editable: Default: `True` :param strip_input: runs the input data through standard python .strip() before passing it to the parse function (can NOT be callable). Default: `True` :param render_value: render the parsed and validated value into a string. Default just converts to unicode: `lambda form, field, value: unicode(value)` :param is_list: interpret request data as a list (can NOT be a callable). Default: `False`` :param read_from_instance: callback to retrieve value from edited instance. Invoked with parameters field and instance. :param write_to_instance: callback to write value to instance. Invoked with parameters field, instance and value. """ model_field = kwargs.get('model_field') if model_field and model_field.remote_field: kwargs['model'] = model_field.remote_field.model super(Field, self).__init__(**kwargs) # value/value_data_list is the final step that contains parsed and valid data self.value = None self._choice_tuples = None self.non_editable_input = Namespace({ **flatten(self.input), **self.non_editable_input, '_name': 'non_editable_input', })() self.input = self.input(_name='input') self.label = self.label(_name='label') @property def form(self): return self.iommi_parent().iommi_parent() # noinspection PyUnusedLocal @staticmethod @refinable def is_valid(form: 'Form', field: 'Field', parsed_data: Any, **_) -> Tuple[bool, str]: return True, '' # noinspection PyUnusedLocal @staticmethod @refinable def parse(form: 'Form', field: 'Field', string_value: str, **_) -> Any: del form, field return string_value @staticmethod @refinable def post_validation(form: 'Form', field: 'Field', **_) -> None: pass @staticmethod @refinable def render_value(form: 'Form', field: 'Field', value: Any) -> str: if isinstance(value, (list, QuerySet)): return ', '.join(field.render_value(form=form, field=field, value=v) for v in value) else: return f'{value}' if value is not None else '' # grab help_text from model if applicable # noinspection PyProtectedMember @staticmethod @evaluated_refinable def help_text(field, **_): if field.model_field is None: return '' return field.model_field.help_text or '' @staticmethod @refinable def read_from_instance(field: 'Field', instance: Any) -> Any: return getattr_path(instance, field.attr) @staticmethod @refinable def write_to_instance(field: 'Field', instance: Any, value: Any) -> None: setattr_path(instance, field.attr, value) def on_bind(self) -> None: assert self.template form = self.iommi_parent().iommi_parent() if self.attr is MISSING: self.attr = self._name if self.display_name is MISSING: self.display_name = capitalize(self._name).replace('_', ' ') if self._name else '' self.errors = Errors(parent=self, **self.errors) if form.editable is False: self.editable = False # Not strict evaluate on purpose self.model = evaluate(self.model, **self.iommi_evaluate_parameters()) self.choices = evaluate_strict(self.choices, **self.iommi_evaluate_parameters()) self.initial = evaluate_strict(self.initial, **self.iommi_evaluate_parameters()) self._read_initial() self._read_raw_data() self.parsed_data = evaluate_strict(self.parsed_data, **self.iommi_evaluate_parameters()) self._parse() self._validate() self.input = self.input.bind(parent=self) self.label = self.label.bind(parent=self) assert not self.label.children self.label.children = dict(text=evaluate_strict(self.display_name, **self.iommi_evaluate_parameters())) self.non_editable_input = self.non_editable_input.bind(parent=self) if self.model and self.include: 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.') def _parse(self): if self.parsed_data is not None: return if not self.editable: return if self.form.mode is INITIALS_FROM_GET and self.raw_data is None and self.raw_data_list is None: return if self.is_list: if self.raw_data_list is not None: self.parsed_data = [self._parse_raw_value(x) for x in self.raw_data_list] else: self.parsed_data = None elif self.is_boolean: self.parsed_data = self._parse_raw_value('0' if self.raw_data is None else self.raw_data) else: if self.raw_data == '' and self.parse_empty_string_as_none: self.parsed_data = None elif self.raw_data is not None: self.parsed_data = self._parse_raw_value(self.raw_data) else: self.parsed_data = None def _parse_raw_value(self, raw_data): try: return self.parse(form=self.form, field=self, string_value=raw_data) except ValueError as e: assert str(e) != '' self.errors.add(str(e)) except ValidationError as e: for message in e.messages: msg = "%s" % message assert msg != '' self.errors.add(msg) def _validate(self): form = self.form if (not self.editable) or (form.mode is INITIALS_FROM_GET and self.raw_data is None and not self.raw_data_list): self.value = self.initial return value = None if self.is_list: if self.parsed_data is not None: value = [self._validate_parsed_data(x) for x in self.parsed_data if x is not None] else: if self.parsed_data is not None: value = self._validate_parsed_data(self.parsed_data) if not self.errors: if form.mode is FULL_FORM_FROM_REQUEST and self.required and value in [None, '']: self.errors.add('This field is required') else: self.value = value def _validate_parsed_data(self, value): is_valid, error = self.is_valid( form=self.form, field=self, parsed_data=value) if is_valid and not self.errors and self.parsed_data is not None and not self.is_list: value = self.parsed_data elif not is_valid and self.form.mode: if not isinstance(error, set): error = {error} for e in error: assert error != '' self.errors.add(e) return value def _read_initial(self): form = self.iommi_parent().iommi_parent() if self.initial is MISSING and self.include and form.instance is not None: if self.attr: initial = self.read_from_instance(self, form.instance) self.initial = initial if self.initial is MISSING: self.initial = None def _read_raw_data(self): if self.raw_data is not None: self.raw_data = evaluate_strict(self.raw_data, **self.iommi_evaluate_parameters()) return if self.raw_data_list is not None: self.raw_data_list = evaluate_strict(self.raw_data_list, **self.iommi_evaluate_parameters()) return form = self.iommi_parent().iommi_parent() if self.is_list: if self.raw_data_list is not None: return try: # django and similar # noinspection PyUnresolvedReferences raw_data_list = form._request_data.getlist(self.iommi_path) except AttributeError: # pragma: no cover # werkzeug and similar raw_data_list = form._request_data.get(self.iommi_path) if raw_data_list and self.strip_input: raw_data_list = [x.strip() for x in raw_data_list] if raw_data_list is not None: self.raw_data_list = raw_data_list else: if self.raw_data is not None: return self.raw_data = form._request_data.get(self.iommi_path) if self.raw_data and self.strip_input: self.raw_data = self.raw_data.strip() def own_evaluate_parameters(self): return dict(field=self) @property def rendered_value(self): if self.errors: return self.raw_data return self.render_value(form=self.form, field=self, value=self.value) @property def choice_to_options_selected(self): if self.value is None: return [] if self.is_list: return [ self.choice_to_option(form=self.iommi_parent(), field=self, choice=v) for v in self.value ] else: return [self.choice_to_option(form=self.iommi_parent(), field=self, choice=self.value)] @property def choice_tuples(self): if self._choice_tuples is not None: return self._choice_tuples self._choice_tuples = [] if not self.required and not self.is_list: self._choice_tuples.append(self.empty_choice_tuple + (0,)) for i, choice in enumerate(self.choices): self._choice_tuples.append(self.choice_to_option(form=self.form, field=self, choice=choice) + (i + 1,)) return self._choice_tuples @classmethod def from_model(cls, model, model_field_name=None, model_field=None, **kwargs): return member_from_model( cls=cls, model=model, factory_lookup=_field_factory_by_field_type, factory_lookup_register_function=register_field_factory, defaults_factory=field_defaults_factory, model_field_name=model_field_name, model_field=model_field, **kwargs) @dispatch( render=EMPTY, ) def __html__(self, *, render=None): assert not render if self.is_boolean: if 'checked' not in self.input.attrs and self.value: self.input.attrs.checked = '' else: if 'value' not in self.input.attrs: self.input.attrs.value = self.rendered_value if not self.editable: self.non_editable_input.children['text'] = self.rendered_value self.input = self.non_editable_input return render_template(self.get_request(), self.template, self.iommi_evaluate_parameters()) @classmethod @class_shortcut( input__attrs__type='hidden', attrs__style__display='none', ) def hidden(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( input__attrs__type='text', ) def text(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( input__tag='textarea', input__attrs__type=None, input__attrs__value=None, input__children__text=lambda field, **_: field.rendered_value, input__attrs__readonly=lambda field, **_: True if field.editable is False else None, ) def textarea(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( parse=int_parse, ) def integer(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( parse=float_parse, ) def float(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( input__attrs__type='password', ) def password(cls, call_target=None, **kwargs): return call_target(**kwargs) # Boolean field. Tries hard to parse a boolean value from its input. @classmethod @class_shortcut( parse=bool_parse, required=False, is_boolean=True, ) def boolean(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( required=True, is_list=False, empty_label='---', is_valid=choice_is_valid, choice_to_option=choice_choice_to_option, parse=choice_parse, ) def choice(cls, call_target=None, **kwargs): """ Shortcut for single choice field. If required is false it will automatically add an option first with the value '' and the title '---'. To override that text pass in the parameter empty_label. :param choice_to_option: callable with three arguments: form, field, choice. Convert from a choice object to a tuple of (choice, value, label, selected), the last three for the <option> element """ assert 'choices' in kwargs setdefaults_path( kwargs, empty_choice_tuple=(None, '', kwargs['empty_label'], True), ) return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute="choice", choices=[True, False], choice_to_option=lambda form, field, choice, **_: ( choice, 'true' if choice else 'false', 'Yes' if choice else 'No', choice == field.value, ), parse=boolean_tristate__parse, required=False, ) def boolean_tristate(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute="choice", parse=choice_queryset__parse, choice_to_option=choice_queryset__choice_to_option, endpoints__choices__func=choice_queryset__endpoint_handler, is_valid=choice_queryset__is_valid, extra__filter_and_sort=choice_queryset__extra__filter_and_sort, extra__model_from_choices=choice_queryset__extra__model_from_choices, ) def choice_queryset(cls, choices, call_target=None, **kwargs): if 'model' not in kwargs: if isinstance(choices, QuerySet): kwargs['model'] = choices.model elif 'model_field' in kwargs: kwargs['model'] = kwargs['model_field'].remote_field.model else: assert False, 'The convenience feature to automatically get the parameter model set only works for QuerySet instances or if you specify model_field' setdefaults_path( kwargs, choices=(lambda form, **_: choices.all()) if isinstance(choices, QuerySet) else choices, # clone the QuerySet if needed ) return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='choice', input__attrs__multiple=True, choice_to_option=multi_choice_choice_to_option, is_list=True, ) def multi_choice(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='choice_queryset', input__attrs__multiple=True, choice_to_option=multi_choice_queryset_choice_to_option, is_list=True, ) def multi_choice_queryset(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='choice', ) def radio(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( parse=datetime_parse, render_value=datetime_render_value, ) def datetime(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( parse=date_parse, render_value=date_render_value, ) def date(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( parse=time_parse, render_value=time_render_value, ) def time(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( parse=decimal_parse, ) def decimal(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( input__attrs__type='url', parse=url_parse, ) def url(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( input__attrs__type='file', raw_data=file__raw_data, write_to_instance=file_write_to_instance, ) def file(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='file', template='iommi/form/image_row.html', ) def image(cls, call_target=None, **kwargs): return call_target(**kwargs) # Shortcut to create a fake input that performs no parsing but is useful to separate sections of a form. @classmethod @class_shortcut( editable=False, attr=None, ) def heading(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( editable=False, attr=None, ) def info(cls, value, call_target=None, **kwargs): """ Shortcut to create an info entry. """ setdefaults_path( kwargs, initial=value, ) return call_target(**kwargs) @classmethod @class_shortcut( input__attrs__type='email', parse=email_parse, ) def email(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( is_valid=phone_number_is_valid, ) def phone_number(cls, call_target=None, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='choice_queryset', ) def foreign_key(cls, model_field, model, call_target, **kwargs): del model 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', ) def many_to_many(cls, call_target, model_field, **kwargs): setdefaults_path( kwargs, choices=model_field.remote_field.model.objects.all(), read_from_instance=many_to_many_factory_read_from_instance, write_to_instance=many_to_many_factory_write_to_instance, extra__django_related_field=True, ) return call_target(model_field=model_field, **kwargs)
class Form(Part): """ Describe a Form. Example: .. code:: python class MyForm(Form): a = Field() b = Field.email() form = MyForm().bind(request=request) You can also create an instance of a form with this syntax if it's more convenient: .. code:: python form = MyForm( fields=dict( a=Field(), b=Field.email(), ] ).bind(request=request) See tri.declarative docs for more on this dual style of declaration. """ actions: Namespace = Refinable() actions_template: Union[str, Template] = Refinable() attrs: Attrs = Refinable() # attrs is evaluated, but in a special way so gets no EvaluatedRefinable type editable: bool = Refinable() h_tag: Union[Fragment, str] = Refinable() # h_tag is evaluated, but in a special way so gets no EvaluatedRefinable type title: Union[Fragment, str] = Refinable() # title is evaluated, but in a special way so gets no EvaluatedRefinable type template: Union[str, Template] = EvaluatedRefinable() model: Type[Model] = Refinable() # model is evaluated, but in a special way so gets no EvaluatedRefinable type member_class: Type[Field] = Refinable() action_class: Type[Action] = Refinable() page_class: Type[Page] = Refinable() class Meta: member_class = Field action_class = Action page_class = Page @reinvokable @dispatch( model=None, editable=True, fields=EMPTY, attrs__action='', attrs__method='post', attrs__enctype='multipart/form-data', actions__submit__call_target__attribute='submit', auto=EMPTY, h_tag__call_target=Header, ) def __init__(self, *, instance=None, fields: Dict[str, Field] = None, _fields_dict: Dict[str, Field] = None, actions: Dict[str, Any] = None, model=None, auto=None, title=MISSING, **kwargs): if auto: auto = FormAutoConfig(**auto) assert not _fields_dict, "You can't have an auto generated Form AND a declarative Form at the same time" assert not model, "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__instance" assert not instance, "You can't use the auto feature and explicitly pass instance. Pass auto__instance (None in the create case)" if auto.model is None: auto.model = auto.instance.__class__ model, fields = self._from_model( model=auto.model, fields=fields, include=auto.include, exclude=auto.exclude, ) instance = auto.instance if title is MISSING and auto.type is not None: title = f'{auto.type.title()} {model._meta.verbose_name}' setdefaults_path( actions, submit__display_name=title, ) super(Form, self).__init__(model=model, title=title, **kwargs) assert isinstance(fields, dict) self.fields = None self.errors: Set[str] = set() self._valid = None self.instance = instance self.mode = INITIALS_FROM_GET collect_members(self, name='actions', items=actions, cls=self.get_meta().action_class) collect_members(self, name='fields', items=fields, items_dict=_fields_dict, cls=self.get_meta().member_class) def on_bind(self) -> None: assert self.actions_template self._valid = None request = self.get_request() self._request_data = request_data(request) self.title = evaluate_strict(self.title, **self.iommi_evaluate_parameters()) if isinstance(self.h_tag, Namespace): if self.title not in (None, MISSING): self.h_tag = self.h_tag( _name='h_tag', children__text=capitalize(self.title), ).bind(parent=self) else: self.h_tag = '' else: self.h_tag = self.h_tag.bind(parent=self) # Actions have to be bound first because is_target() needs it bind_members(self, name='actions', cls=Actions) if self._request_data is not None and self.is_target(): self.mode = FULL_FORM_FROM_REQUEST bind_members(self, name='fields') bind_members(self, name='endpoints') self.is_valid() self.errors = Errors(parent=self, errors=self.errors) def own_evaluate_parameters(self): return dict(form=self) # property for jinja2 compatibility @property def render_actions(self): assert self._is_bound, 'The form has not been bound. You need to call bind() before you can render it.' non_grouped_actions, grouped_actions = group_actions(self.actions) return render_template( self.get_request(), self.actions_template, dict( actions=self.iommi_bound_members().actions, non_grouped_actions=non_grouped_actions, grouped_actions=grouped_actions, form=self, )) @classmethod @dispatch( fields=EMPTY, ) def fields_from_model(cls, fields, **kwargs): return create_members_from_model( member_class=cls.get_meta().member_class, member_params_by_member_name=fields, **kwargs ) @classmethod @dispatch( fields=EMPTY, ) def _from_model(cls, model, *, fields, include=None, exclude=None): fields = cls.fields_from_model(model=model, include=include, exclude=exclude, fields=fields) return model, fields def is_target(self): return any(action.is_target() for action in values(self.actions)) def is_valid(self): if self._valid is None: self.validate() for field in values(self.fields): if field.errors: self._valid = False break else: self._valid = not self.errors return self._valid def validate(self): for field in values(self.fields): field.post_validation(**field.iommi_evaluate_parameters()) self.post_validation(**self.iommi_evaluate_parameters()) return self @staticmethod @refinable def post_validation(form, **_): pass def add_error(self, msg): self.errors.add(msg) # property for jinja2 compatibility @property def render_fields(self): r = [] for field in values(self.fields): r.append(field.__html__()) # We need to preserve all other GET parameters, so we can e.g. filter in two forms on the same page, and keep sorting after filtering own_field_paths = {f.iommi_path for f in values(self.fields)} for k, v in items(self.get_request().GET): if k not in own_field_paths and not k.startswith('-'): r.append(format_html('<input type="hidden" name="{}" value="{}" />', k, v)) return format_html('{}\n' * len(r), *r) @dispatch( render__call_target=render_template, ) def __html__(self, *, render=None): setdefaults_path( render, template=self.template, context=self.iommi_evaluate_parameters().copy(), ) request = self.get_request() render.context.update(csrf(request)) return render(request=request) def apply(self, instance): """ Write the new values specified in the form into the instance specified. """ assert self.is_valid() for field in values(self.fields): self.apply_field(instance=instance, field=field) return instance @staticmethod def apply_field(instance, field): if not field.editable: field.value = field.initial if field.attr is not None: field.write_to_instance(field, instance, field.value) def get_errors(self): self.is_valid() r = {} if self.errors: r['global'] = self.errors field_errors = {x._name: x.errors for x in values(self.fields) if x.errors} if field_errors: r['fields'] = field_errors return r @classmethod @class_shortcut( extra__on_save=lambda **kwargs: None, # pragma: no mutate extra__redirect=lambda redirect_to, **_: HttpResponseRedirect(redirect_to), extra__redirect_to=None, auto=EMPTY, ) def crud(cls, call_target, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='crud', extra__is_create=True, actions__submit__post_handler=create_object__post_handler, auto__type='create', ) def create(cls, call_target, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='crud', extra__is_create=False, actions__submit__post_handler=edit_object__post_handler, auto__type='edit', ) def edit(cls, call_target, **kwargs): return call_target(**kwargs) @classmethod @class_shortcut( call_target__attribute='crud', actions__submit__call_target__attribute='delete', actions__submit__post_handler=delete_object__post_handler, auto__type='delete', editable=False, ) def delete(cls, call_target, **kwargs): return call_target(**kwargs) def as_view(self): return build_as_view_wrapper(self)
class Part(Traversable): """ `Part` is the base class for parts of a page that can be rendered as html, and can respond to ajax and post. """ include: bool = Refinable( ) # This is evaluated, but first and in a special way after: Union[int, str] = EvaluatedRefinable() extra: Dict[str, Any] = Refinable() extra_evaluated: Dict[str, Any] = Refinable( ) # not EvaluatedRefinable because this is an evaluated container so is special endpoints: Namespace = Refinable() @reinvokable @dispatch( extra=EMPTY, include=True, ) def __init__(self, *, endpoints: Dict[str, Any] = None, include, **kwargs): super(Part, self).__init__(include=include, **kwargs) collect_members(self, name='endpoints', items=endpoints, cls=Endpoint) if iommi_debug_on(): import inspect self._instantiated_at_frame = inspect.currentframe().f_back @dispatch( render=EMPTY, ) @abstractmethod def __html__(self, *, render=None): assert False, 'Not implemented' # pragma: no cover, no mutate def __str__(self): assert self._is_bound, 'This object is unbound, you probably forgot to call `.bind(request=request)` on it' return self.__html__() def bind(self, *, parent=None, request=None): result = super(Part, self).bind(parent=parent, request=request) if result is None: return None del self bind_members(result, name='endpoints') return result @dispatch def render_to_response(self, **kwargs): request = self.get_request() req_data = request_data(request) def dispatch_response_handler(r): if isinstance(r, HttpResponseBase): return r elif isinstance(r, Part): # We can't do r.bind(...).render_to_response() because then we recurse in here # r also has to be bound already return HttpResponse(render_root(part=r, **kwargs)) else: return HttpResponse(json.dumps(r), content_type='application/json') if request.method == 'GET': dispatch_prefix = DISPATCH_PATH_SEPARATOR dispatcher = perform_ajax_dispatch dispatch_error = 'Invalid endpoint path' elif request.method == 'POST': dispatch_prefix = '-' dispatcher = perform_post_dispatch dispatch_error = 'Invalid post path' else: # pragma: no cover assert False # This has already been checked in request_data() dispatch_commands = { key: value for key, value in items(req_data) 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) else: if request.method == 'POST': assert False, 'This request was a POST, but there was no dispatch command present.' return HttpResponse(render_root(part=self, **kwargs))