Exemplo n.º 1
0
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)
            })
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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)
Exemplo n.º 8
0
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__()
Exemplo n.º 9
0
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)
Exemplo n.º 10
0
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)
Exemplo n.º 11
0
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)
Exemplo n.º 12
0
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)
Exemplo n.º 13
0
 class Foo(Traversable):
     bar = EvaluatedRefinable()
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
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)
Exemplo n.º 16
0
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))