Esempio n. 1
0
class AutoConfig(RefinableObject):
    model: Type[Model] = Refinable()  # model is evaluated, but in a special way so gets no EvaluatedRefinable type
    include = Refinable()
    exclude = Refinable()

    @dispatch
    def __init__(self, **kwargs):
        super(AutoConfig, self).__init__(**kwargs)
Esempio n. 2
0
    class MyClass(RefinableObject):
        foo = Refinable()
        container = Refinable()

        def bind(self, container):
            new_object = copy.copy(self)
            new_object.container = container
            return new_object
Esempio n. 3
0
    class MyClass(RefinableObject):
        foo = Refinable()
        container = Refinable()

        # noinspection PyShadowingNames
        def bind(self, container):
            new_object = copy.copy(self)
            new_object.container = container
            return new_object
Esempio n. 4
0
    class MyClass(RefinableObject):
        x = Refinable()
        y = Refinable()

        @dispatch(
            x=17,
            y=EMPTY,
        )
        def __init__(self, **kwargs):
            super(MyClass, self).__init__(**kwargs)
Esempio n. 5
0
    class A(RefinableObject):
        foo = Refinable()
        bar = Refinable()

        @classmethod
        @class_shortcut
        def shortcut1(cls, call_target, **kwargs):
            return call_target(**kwargs)

        def items(self):
            return dict(foo=self.foo, bar=self.bar)
Esempio n. 6
0
    class Foo(RefinableObject):
        """
        docstring for Foo
        """

        name = Refinable()
        description = Refinable()
        some_other_thing = Refinable()
        empty_string_default = Refinable()

        @dispatch(
            name='foo-name',
            description=lambda foo, bar: 'qwe',
            some_other_thing=some_callable,
            empty_string_default='',
        )
        def __init__(self):
            """
            :param name: description of the name field
            """
            super(Foo, self).__init__()  # pragma: no cover

        @staticmethod
        @refinable
        def refinable_func(field, instance, value):
            pass  # pragma: no cover

        @classmethod
        @class_shortcut
        def shortcut1(cls):
            return cls()  # pragma: no cover

        @classmethod
        @class_shortcut(description='fish')
        def shortcut2(cls, call_target):
            """shortcut2 docstring"""
            return call_target()  # pragma: no cover

        @classmethod
        # fmt: off
        @class_shortcut(description=lambda foo: 'qwe')
        # fmt: on
        def shortcut3(cls, call_target):
            """
            shortcut3 docstring

            :param call_target: something something call_target
            """
            return call_target()  # pragma: no cover
Esempio n. 7
0
    class Foo(RefinableObject):
        """docstring for Foo"""

        name = Refinable()
        description = Refinable()
        some_other_thing = Refinable()

        @dispatch(
            name='foo-name',
            description=lambda foo, bar: 'qwe',
            some_other_thing=some_callable,
        )
        def __init__(self):
            """
            :param name: description of the name field
            """
            super(Foo, self).__init__()

        @staticmethod
        @refinable
        def refinable_func(field, instance, value):
            pass

        @classmethod
        @class_shortcut
        def shortcut1(cls):
            return cls()

        @classmethod
        @class_shortcut(
            description='fish'
        )
        def shortcut2(cls, call_target):
            """shortcut2 docstring"""
            return call_target()

        @classmethod
        @class_shortcut(
            description=lambda foo: 'qwe'
            # TODO: This is currently not shown in the documentation output, but it should be!
        )
        def shortcut3(cls, call_target):
            """
            shortcut3 docstring

            :param call_target: something something call_target
            """
            return call_target()
Esempio n. 8
0
    class A(Traversable):
        @dispatch
        @reinvokable
        def __init__(self, **kwargs):
            super().__init__(**kwargs)

        foo = Refinable()
        bar = Refinable()

        @classmethod
        @class_shortcut
        def shortcut1(cls, call_target, **kwargs):
            return call_target(**kwargs)

        def items(self):
            return dict(foo=self.foo, bar=self.bar)
Esempio n. 9
0
    class ReinvokableWithExtra(Namespace):
        _name = None
        extra = Refinable()

        @reinvokable
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
Esempio n. 10
0
    class Foo(RefinableObject):
        name = Refinable()

        @dispatch(
            # this is to handle that mutmut mutates strip(',') to strip('XX,XX')
            name=lambda X: X, )
        def __init__(self, **kwargs):
            super(Foo, self).__init__(**kwargs)  # pragma: no cover
Esempio n. 11
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"`.
    """

    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)
Esempio n. 12
0
class Fragment(PagePart):
    attrs: Dict[str, Any] = Refinable()
    tag = Refinable()
    template: Union[str, Template] = Refinable()

    @dispatch(
        attrs=EMPTY,
        tag=None,
    )
    def __init__(self,
                 child: PartType = None,
                 *,
                 children: Optional[List[PartType]] = None,
                 **kwargs):
        super(Fragment, self).__init__(**kwargs)

        self._children = [
        ]  # TODO: _children to avoid colliding with PageParts children() API. Not nice. We should do something nicer here.
        if child is not None:
            self._children.append(child)

        self._children.extend(children or [])

    def render_text_or_children(self, context):
        return format_html(
            '{}' * len(self._children),
            *[as_html(part=x, context=context) for x in self._children])

    def __repr__(self):
        return f'<Fragment: tag:{self.tag}, attrs:{self.attrs.items()}>'

    def on_bind(self) -> None:
        self.attrs = evaluate_attrs(self, **self.evaluate_attribute_kwargs())
        # TODO: do we want to do this?
        # self._children = [evaluate_strict(x, **self.evaluate_attribute_kwargs()) for x in self._children]

    @dispatch(
        context=EMPTY,
        render=fragment__render,
    )
    def __html__(self, *, context=None, render=None):
        return render(fragment=self, context=context)

    def _evaluate_attribute_kwargs(self):
        return dict(fragment=self)
Esempio n. 13
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)
            })
Esempio n. 14
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)
Esempio n. 15
0
        class Fruit(Traversable):
            attrs = Refinable()

            @reinvokable
            @dispatch(
                attrs__class__fruit=True, )
            def __init__(self, **kwargs):
                super(Fruit, self).__init__(**kwargs)

            @classmethod
            @class_shortcut(
                attrs__class__some_fruit=True, )
            def some_fruit(cls, *, call_target=None, **kwargs):
                return call_target(**kwargs)
Esempio n. 16
0
    class Foo(RefinableObject):
        a = Refinable()
        b = Refinable()

        @dispatch(
            b='default_b', )
        def __init__(self, **kwargs):
            self.non_refinable = 17
            super(Foo, self).__init__(**kwargs)

        @staticmethod
        @dispatch(f=Namespace(call_target=f_))
        @refinable
        def c(f):
            """
            c docstring
            """
            return f()

        @staticmethod
        @shortcut
        @dispatch(call_target=f_)
        def shortcut_to_f(call_target):
            return call_target()
Esempio n. 17
0
    class Foo(RefinableObject):
        """
        First description
        """

        name = Refinable()

        @dispatch
        def __init__(self, **kwargs):
            """
            __init__ description

            :param foo: foo description
            """
            super(Foo, self).__init__(**kwargs)
Esempio n. 18
0
    class Basket(Traversable):
        fruits = Refinable()

        class Meta:
            fruits__banana__attrs__class__basket = True

        @dispatch(
            fruits=EMPTY,
        )
        def __init__(self, *, _fruits_dict, fruits, **kwargs):
            super(Basket, self).__init__(**kwargs)
            collect_members(self, name='fruits', items_dict=_fruits_dict, items=fruits, cls=Fruit)

        def on_bind(self) -> None:
            bind_members(self, name='fruits')
Esempio n. 19
0
class Advanced(Fragment):
    toggle: Namespace = Refinable()

    @dispatch(toggle=EMPTY)
    @reinvokable
    def __init__(self, **kwargs):
        super(Advanced, self).__init__(**kwargs)

        toggle = setdefaults_path(
            Namespace(),
            self.toggle,
            _name='toggle',
            call_target=Action,
            attrs__href='#',
            attrs__class__iommi_query_toggle_simple_mode=True,
            attrs={'data-advanced-mode': 'simple'},
            display_name=gettext('Switch to advanced search'),
        )
        self.toggle = declared_members(self).toggle = toggle()

    def on_bind(self) -> None:
        super(Advanced, self).on_bind()
        self.toggle = self._bound_members.toggle = self.toggle.bind(parent=self)
Esempio n. 20
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)
Esempio n. 21
0
class Traversable(RefinableObject):
    """
    Abstract API for objects that have a place in the iommi path structure.
    You should not need to care about this class as it is an implementation
    detail.
    """

    _name = None
    _parent = None
    _is_bound = False
    _request = None
    context = None

    iommi_style: str = Refinable()

    _declared_members: Dict[str, 'Traversable']
    _bound_members: Dict[str, 'Traversable']

    @dispatch
    def __init__(self, _name=None, **kwargs):
        self._declared_members = Struct()
        self._bound_members = Struct()
        self._evaluate_parameters = None
        self._name = _name

        super(Traversable, self).__init__(**kwargs)

    def __repr__(self):
        n = f' {self._name}' if self._name is not None else ''
        b = ' (bound)' if self._is_bound else ''
        try:
            p = f" path:'{self.iommi_path}'" if self.iommi_parent(
            ) is not None else ""
        except PathNotFoundException:
            p = ' path:<no path>'
        c = ''
        if self._is_bound and hasattr(self, '_bound_members'):
            members = self._bound_members
            if members:
                c = f" members:{list(members.keys())!r}"

        return f'<{type(self).__module__}.{type(self).__name__}{n}{b}{p}{c}>'

    def iommi_name(self) -> str:
        return self._name

    def iommi_parent(self) -> "Traversable":
        return self._parent

    def iommi_root(self) -> 'Traversable':
        node = self
        while node.iommi_parent() is not None:
            node = node.iommi_parent()
        return node

    def iommi_bound_members(self) -> Dict[str, 'Traversable']:
        return self._bound_members if self._bound_members is not None else Struct(
        )

    @property
    def iommi_path(self) -> str:
        long_path = build_long_path(self)
        path_by_long_path = get_path_by_long_path(self)
        path = path_by_long_path.get(long_path)
        if path is None:
            candidates = '\n'.join(path_by_long_path.keys())
            raise PathNotFoundException(
                f"Path not found(!) (Searched for {long_path} among the following:\n{candidates}"
            )
        return path

    @property
    def iommi_dunder_path(self) -> str:
        assert self._is_bound
        return build_long_path(self).replace('/', '__')

    def reinvoke(self, additional_kwargs: Dict[str, Any]) -> "Traversable":
        assert hasattr(
            self, '_iommi_saved_params'
        ), f'reinvoke() called on class with missing @reinvokable decorator: {self.__class__.__name__}'
        additional_kwargs_namespace = Namespace(additional_kwargs)
        kwargs = {}
        for name, saved_param in items(self._iommi_saved_params):
            try:
                new_param = getattr_path(additional_kwargs_namespace, name)
            except AttributeError:
                kwargs[name] = saved_param
            else:
                if hasattr(saved_param, 'reinvoke'):
                    assert isinstance(new_param, dict)
                    kwargs[name] = saved_param.reinvoke(new_param)
                else:
                    if isinstance(saved_param, Namespace):
                        kwargs[name] = Namespace(saved_param, new_param)
                    else:
                        kwargs[name] = new_param

        additional_kwargs_namespace.pop('call_target', None)

        kwargs = Namespace(
            additional_kwargs_namespace,
            kwargs)  # Also include those keys not already in the original

        result = type(self)(**kwargs)

        result._name = self._name
        __tri_declarative_shortcut_stack = getattr(
            self, '__tri_declarative_shortcut_stack', None)
        if __tri_declarative_shortcut_stack is not None:
            setattr(result, '__tri_declarative_shortcut_stack',
                    __tri_declarative_shortcut_stack)

        return result

    def bind(self, *, parent=None, request=None):
        assert parent is None or parent._is_bound
        assert not self._is_bound

        if parent is None:
            self._request = request
            if self._name is None:
                self._name = 'root'

        result = copy.copy(self)
        result._declared = self

        del self  # to prevent mistakes when changing the code below

        result._parent = parent
        result._is_bound = True

        evaluate_parameters = {
            'traversable': result,
            **(parent.iommi_evaluate_parameters() if parent is not None else {}),
            **result.own_evaluate_parameters(),
        }
        if parent is None:
            evaluate_parameters['request'] = request
        result._evaluate_parameters = evaluate_parameters

        if hasattr(result, 'include'):
            include = evaluate_strict(result.include, **evaluate_parameters)
            if not bool(include):
                return None
        else:
            include = MISSING

        if include is not MISSING:
            result.include = True

        rest_of_style = apply_style(result)

        # Styling has another chance of setting include to False
        if include is not MISSING and result.include is False:
            return None
        result.include = True

        result.on_bind()

        if rest_of_style:
            rest = apply_style_recursively(style_data=rest_of_style,
                                           obj=result)
            assert not rest, f'There is still styling data left for {result}: {rest_of_style}'

        # on_bind has a chance to hide itself
        if result.include is False:
            return None

        if hasattr(result, 'attrs'):
            result.attrs = evaluate_attrs(result,
                                          **result.iommi_evaluate_parameters())

        evaluated_attributes = [
            k for k, v in items(result.get_declared('refinable_members'))
            if is_evaluated_refinable(v)
        ]
        evaluate_members(result, evaluated_attributes, **evaluate_parameters)

        if hasattr(result, 'extra_evaluated'):
            result.extra_evaluated = evaluate_strict_container(
                result.extra_evaluated or {}, **evaluate_parameters)

        return result

    def on_bind(self) -> None:
        pass

    def own_evaluate_parameters(self):
        return {}

    def iommi_evaluate_parameters(self):
        return self._evaluate_parameters

    def get_request(self):
        if self._parent is None:
            return self._request
        else:
            return self.iommi_root().get_request()

    def get_context(self):
        if self._parent is None:
            return self.context or {}
        else:
            return self.iommi_parent().get_context()
Esempio n. 22
0
 class Foo(RefinableObject):
     name = Refinable()
Esempio n. 23
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
Esempio n. 24
0
class QueryAutoConfig(AutoConfig):
    rows = Refinable()
Esempio n. 25
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)
Esempio n. 26
0
class Admin(Page):
    class Meta:
        iommi_style = 'bootstrap'
        table_class = Table
        form_class = Form
        apps__auth_user__include = True
        apps__auth_group__include = True
        parts__messages = Messages()
        parts__list_auth_user = dict(
            auto__include=['username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active', 'is_superuser'],
            columns=dict(
                username__filter__freetext=True,
                email__filter__freetext=True,
                first_name__filter__freetext=True,
                last_name__filter__freetext=True,
                is_staff__filter__include=True,
                is_active__filter__include=True,
                is_superuser__filter__include=True,
            ),
        )

    table_class: Type[Table] = Refinable()
    form_class: Type[Form] = Refinable()

    apps: Namespace = Refinable()  # Global configuration on apps level

    menu = Menu(
        sub_menu=dict(
            root=MenuItem(url=lambda admin, **_: reverse(admin.__class__.all_models), display_name=gettext('iommi administration')),
            change_password=MenuItem(url=lambda **_: reverse(Auth.change_password), display_name=gettext('Change password')),
            logout=MenuItem(url=lambda **_: reverse(Auth.logout), display_name=gettext('Logout')),
        ),
    )

    @read_config
    @reinvokable
    @dispatch(
        apps=EMPTY,
        parts=EMPTY,
    )
    def __init__(self, parts, apps, **kwargs):
        # Validate apps params
        for k in apps.keys():
            assert k in joined_app_name_and_model, f'{k} is not a valid app/model key.\n\nValid keys:\n    ' + '\n    '.join(joined_app_name_and_model)

        def should_throw_away(k, v):
            if isinstance(v, Namespace) and 'call_target' in v:
                return False

            if k == 'all_models':
                return True

            prefix_blacklist = [
                'list_',
                'delete_',
                'create_',
                'edit_',
            ]
            for prefix in prefix_blacklist:
                if k.startswith(prefix):
                    return True

            return False

        parts = {
            # Arguments that are not for us needs to be thrown on the ground
            k: None if should_throw_away(k, v) else v
            for k, v in items(parts)
        }

        super(Admin, self).__init__(parts=parts, apps=apps, **kwargs)

    @staticmethod
    def has_permission(request, operation, model=None, instance=None):
        return request.user.is_staff

    def own_evaluate_parameters(self):
        return dict(admin=self, **super(Admin, self).own_evaluate_parameters())

    @classmethod
    @class_shortcut(
        table=EMPTY,
        table__call_target__attribute='div',
    )
    @require_login
    def all_models(cls, request, table, call_target=None, **kwargs):
        if not cls.has_permission(request, operation='all_models'):
            raise Http404()

        def rows(admin, **_):

            for app_name, models in items(django_apps.all_models):
                has_yielded_header = False

                for model_name, model in sorted(items(models), key=lambda x: x[1]._meta.verbose_name_plural):
                    if not admin.apps.get(f'{app_name}_{model_name}', {}).get('include', False):
                        continue

                    if not has_yielded_header:
                        yield Struct(
                            name=app_verbose_name_by_label[app_name],
                            verbose_app_name=app_verbose_name_by_label[app_name],
                            url=None,
                            format=lambda row, table, **_: Header(row.name, _name='invalid_name').bind(parent=table).__html__()
                        )
                        has_yielded_header = True

                    yield Struct(
                        verbose_app_name=app_verbose_name_by_label[app_name],
                        app_name=app_name,
                        name=model._meta.verbose_name_plural.capitalize(),
                        url='%s/%s/' % (app_name, model_name),
                        format=lambda row, **_: row.name,
                    )

        table = setdefaults_path(
            Namespace(),
            table,
            title=gettext('All models'),
            call_target__cls=cls.get_meta().table_class,
            sortable=False,
            rows=rows,
            header__template=None,
            page_size=None,
            columns__name=dict(
                cell__url=lambda row, **_: row.url,
                display_name='',
                cell__format=lambda row, **kwargs: row.format(row=row, **kwargs),
            ),
        )

        return call_target(
            parts__all_models=table,
            **kwargs
        )

    @classmethod
    @class_shortcut(
        table=EMPTY,
    )
    @require_login
    def list(cls, request, app_name, model_name, table, call_target=None, **kwargs):
        model = django_apps.all_models[app_name][model_name]

        if not cls.has_permission(request, operation='list', model=model):
            raise Http404()

        table = setdefaults_path(
            Namespace(),
            table,
            call_target__cls=cls.get_meta().table_class,
            auto__model=model,
            columns=dict(
                select__include=True,
                edit=dict(
                    call_target__attribute='edit',
                    after=0,
                    cell__url=lambda row, **_: '%s/edit/' % row.pk,
                ),
                delete=dict(
                    call_target__attribute='delete',
                    after=LAST,
                    cell__url=lambda row, **_: '%s/delete/' % row.pk,
                ),
            ),
            actions=dict(
                create=dict(
                    display_name=gettext('Create %(model_name)s') % dict(model_name=model._meta.verbose_name),
                    attrs__href='create/',
                ),
            ),
            query_from_indexes=True,
            bulk__actions__delete__include=True,
        )

        return call_target(
            parts__header__children__link__attrs__href='../..',
            **{f'parts__list_{app_name}_{model_name}': table},
            **kwargs,
        )

    @classmethod
    @class_shortcut(
        form=EMPTY,
    )
    @require_login
    def crud(cls, request, operation, form, app_name, model_name, pk=None, call_target=None, **kwargs):
        model = django_apps.all_models[app_name][model_name]
        instance = model.objects.get(pk=pk) if pk is not None else None

        if not cls.has_permission(request, operation=operation, model=model, instance=instance):
            raise Http404()

        def on_save(form, instance, **_):
            message = f'{form.model._meta.verbose_name.capitalize()} {instance} was ' + ('created' if form.extra.is_create else 'updated')
            messages.add_message(request, messages.INFO, message, fail_silently=True)

        def on_delete(form, instance, **_):
            message = f'{form.model._meta.verbose_name.capitalize()} {instance} was deleted'
            messages.add_message(request, messages.INFO, message, fail_silently=True)

        form = setdefaults_path(
            Namespace(),
            form,
            call_target__cls=cls.get_meta().form_class,
            auto__instance=instance,
            auto__model=model,
            call_target__attribute=operation,
            extra__on_save=on_save,
            extra__on_delete=on_delete,
        )

        return call_target(
            **{f'parts__{operation}_{app_name}_{model_name}': form},
            **kwargs,
        )

    @classmethod
    @class_shortcut(
        call_target__attribute='crud',
        operation='create',
        parts__header__children__link__attrs__href='../../..',
    )
    def create(cls, request, call_target, **kwargs):
        return call_target(request=request, **kwargs)

    @classmethod
    @class_shortcut(
        call_target__attribute='crud',
        operation='edit',
        parts__header__children__link__attrs__href='../../../..',
    )
    def edit(cls, request, call_target, **kwargs):
        return call_target(request=request, **kwargs)

    @classmethod
    @class_shortcut(
        call_target__attribute='crud',
        operation='delete',
        parts__header__children__link__attrs__href='../../../..',
    )
    def delete(cls, request, call_target, **kwargs):
        return call_target(request=request, **kwargs)

    @classmethod
    def urls(cls):
        return Struct(
            urlpatterns=[
                            path('', cls.all_models),
                            path('<app_name>/<model_name>/', cls.list),
                            path('<app_name>/<model_name>/create/', cls.create),
                            path('<app_name>/<model_name>/<int:pk>/edit/', cls.edit),
                            path('<app_name>/<model_name>/<int:pk>/delete/', cls.delete),
                        ] + Auth.urls().urlpatterns
        )
Esempio n. 27
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__()
Esempio n. 28
0
class Menu(MenuBase):
    """
    Class that describes menus.

    Example:

    .. code:: python

        menu = Menu(
            sub_menu=dict(
                root=MenuItem(url='/'),

                albums=MenuItem(url='/albums/'),

                # url defaults to /<name>/ so we
                # don't need to write /musicians/ here
                musicians=MenuItem(),
            ),
        )
    """
    items_container = Refinable()

    @reinvokable
    @dispatch(
        sort=False,
        items_container=EMPTY,
    )
    def __init__(self, **kwargs):
        super(Menu, self).__init__(**kwargs)
        self.fragment = None

    def __html__(self, *, render=None):
        return self.fragment.__html__()

    def own_evaluate_parameters(self):
        return dict(menu=self)

    def on_bind(self):
        super(Menu, self).on_bind()

        self.fragment = Fragment(
            _name=self._name,
            tag=self.tag,
            template=self.template,
            attrs=self.attrs,
            children__items_container=Fragment(**self.items_container, )).bind(
                parent=self)
        # need to do this here because otherwise the sub menu will get get double bind
        items_container = self.fragment.children.items_container
        for name, item in items(self.sub_menu):
            assert name not in items_container.children
            items_container.children[name] = item

        self.set_active(current_path=self.get_request().path)

    def validate(self):
        # verify there is no ambiguity for the MenuItems
        paths = defaultdict(list)

        def _validate(item):
            for sub_item in values(item.sub_menu):
                if sub_item.url is None or '://' in sub_item.url or sub_item.url.startswith(
                        '#'):
                    continue

                _validate(sub_item)

                path = urlparse(sub_item.url).path
                paths[path].append(sub_item.iommi_path)

        _validate(self)

        ambiguous = {k: v for k, v in items(paths) if len(v) > 1}
        return ambiguous

    def set_active(self, current_path: str):
        current = None
        current_parts_matching = 0
        path_parts = PurePosixPath(current_path).parts

        def _set_active(item):
            nonlocal current_parts_matching
            nonlocal current
            for sub_item in values(item.sub_menu):
                _set_active(sub_item)

                if sub_item.url is None or '://' in sub_item.url:
                    continue

                parsed_url = urlparse(sub_item.url).path

                if current_path.startswith(parsed_url):
                    parts = PurePosixPath(unquote(parsed_url)).parts
                    matching_parts = 0
                    for item in range(min(len(parts), len(path_parts))):
                        if parts[item] is path_parts[item]:
                            matching_parts += 1

                    if matching_parts > current_parts_matching:
                        current = sub_item
                        current_parts_matching = matching_parts

        _set_active(self)

        if current:
            current._active = True
Esempio n. 29
0
class PagePart(RefinableObject):
    name: str = Refinable()
    include: bool = Refinable()
    after: Union[int, str] = Refinable()
    default_child = Refinable()
    extra: Namespace = Refinable()
    extra_evaluated: Namespace = Refinable()
    style: str = Refinable()

    parent = None
    _is_bound = False

    @dispatch(
        extra=EMPTY,
        extra_evaluated=EMPTY,
        include=True,
        name=None,
    )
    def __init__(self, **kwargs):
        super(PagePart, self).__init__(**kwargs)

    @dispatch(
        context=EMPTY,
        render=EMPTY,
    )
    def __html__(self, *, context=None, render=None):
        assert False, 'Not implemented'

    def __str__(self):
        assert self._is_bound
        return self.__html__()

    @dispatch
    def render_to_response(self, **kwargs):
        request = self.request()
        req_data = request_data(request)

        if request.method == 'GET':
            dispatch_prefix = DISPATCH_PATH_SEPARATOR
            dispatcher = perform_ajax_dispatch
            dispatch_error = 'Invalid endpoint path'

            def dispatch_response_handler(r):
                return HttpResponse(json.dumps(r), content_type='application/json')

        elif request.method == 'POST':
            dispatch_prefix = '-'
            dispatcher = perform_post_dispatch
            dispatch_error = 'Invalid post path'

            def dispatch_response_handler(r):
                return r

        else:
            assert False  # This has already been checked in request_data()

        dispatch_commands = {key: value for key, value in req_data.items() if key.startswith(dispatch_prefix)}
        assert len(dispatch_commands) in (0, 1), 'You can only have one or no dispatch commands'
        if dispatch_commands:
            dispatch_target, value = next(iter(dispatch_commands.items()))
            try:
                result = dispatcher(root=self, path=dispatch_target, value=value)
            except InvalidEndpointPathException:
                if settings.DEBUG:
                    raise
                result = dict(error=dispatch_error)

            if result is not None:
                return dispatch_response_handler(result)

        return HttpResponse(render_root(part=self, **kwargs))

    def bind(self, *, parent=None, request=None):
        assert parent is None or parent._is_bound
        assert not self._is_bound

        if parent is None:
            self._request = request
            if self.name is None:
                self.name = 'root'
            if self.default_child is None:
                self.default_child = True

        if hasattr(self, '_no_copy_on_bind'):
            result = self
        else:
            result = copy.copy(self)
            result._declared = self
        del self  # to prevent mistakes when changing the code below

        result.parent = parent
        result._is_bound = True

        apply_style(result)
        result.on_bind()

        if len(result.children()) == 1:
            for the_only_part in result.children().values():
                if the_only_part.default_child is None:
                    the_only_part.default_child = True

        return result

    def on_bind(self) -> None:
        pass

    def children(self):
        assert self._is_bound

        return Struct()

    def request(self):
        if self.parent is None:
            return self._request
        else:
            return self.parent.request()

    def dunder_path(self) -> str:
        assert self._is_bound
        if self.parent is not None:
            return path_join(self.parent.dunder_path(), self.name, separator='__')
        else:
            assert self.name, f'{self} is missing a name, but it was asked about its path'
            return ''

    def path(self) -> str:
        assert self._is_bound
        if self.default_child:
            if self.parent is not None:
                return self.parent.path()
            else:
                return ''

        if self.parent is not None:
            return path_join(self.parent.path(), self.name)
        else:
            assert self.name, f'{self} is missing a name, but it was asked about its path'
            return self.name

    def endpoint_path(self):
        return DISPATCH_PREFIX + self.path()

    def evaluate_attribute_kwargs(self):
        return {**self._evaluate_attribute_kwargs(), **(self.parent.evaluate_attribute_kwargs() if self.parent is not None else {})}

    def _evaluate_attribute_kwargs(self):
        return {}

    def _evaluate_attribute(self, key, strict=True):
        evaluate_member(self, key, **self.evaluate_attribute_kwargs(), strict=strict)

    def _evaluate_include(self):
        self._evaluate_attribute('include')
Esempio n. 30
0
class Page(PagePart):
    member_class: Type[Fragment] = Refinable()

    class Meta:
        member_class = Fragment

    @dispatch(
        parts=EMPTY, )
    def __init__(self,
                 *,
                 _parts_dict: Dict[str, PartType] = None,
                 parts: dict,
                 **kwargs):
        super(Page, self).__init__(**kwargs)

        self.parts = {
        }  # This is just so that the repr can survive if it gets triggered before parts is set properly

        # First we have to up sample parts that aren't PagePart into Fragment
        def as_fragment_if_needed(k, v):
            if not isinstance(v, PagePart):
                return Fragment(v, name=k)
            else:
                return v

        _parts_dict = {
            k: as_fragment_if_needed(k, v)
            for k, v in _parts_dict.items()
        }
        parts = Namespace(
            {k: as_fragment_if_needed(k, v)
             for k, v in parts.items()})

        self._columns_unapplied_data = {}
        self.declared_parts: Dict[str, PartType] = collect_members(
            items=parts,
            items_dict=_parts_dict,
            cls=self.get_meta().member_class,
            unapplied_config=self._columns_unapplied_data)

    def on_bind(self) -> None:
        bind_members(self, name='parts', default_child=True)

    def __repr__(self):
        return f'<Page with parts: {list(self.parts.keys())}>'

    def children(self):
        assert self._is_bound
        return self.parts

    def _evaluate_attribute_kwargs(self):
        return dict(page=self)

    @dispatch(context=EMPTY,
              render=lambda rendered: format_html('{}' * len(rendered),
                                                  *rendered.values()))
    def __html__(self, *, context=None, render=None):
        rendered = {}
        for part in self.parts.values():
            assert part.name not in context
            rendered[part.name] = as_html(part=part, context=context)

        return render(rendered)