Example #1
0
def test_invalid_value():
    request = Struct(method='GET', GET=Data(**{'query': 'bazaar=asd'}))
    # noinspection PyTypeChecker
    query2 = Query(request=request, variables=[Variable.integer(name='bazaar', value_to_q=lambda variable, op, value_string_or_f: None)])
    with pytest.raises(QueryException) as e:
        query2.to_q()
    assert 'Unknown value "asd" for variable "bazaar"' in str(e)
Example #2
0
    def prepare(self, request):
        if self._has_prepared:
            return

        self.request = request

        def bind_columns():
            for index, column in enumerate(self.columns):
                values = evaluate_recursive(Struct(column), table=self, column=column)
                values = setdefaults_path(
                    Struct(), self.column.get(column.name, {}), values, column=column, table=self, index=index
                )
                yield BoundColumn(**values)

        self.bound_columns = list(bind_columns())
        self.bound_column_by_name = OrderedDict(
            (bound_column.name, bound_column) for bound_column in self.bound_columns
        )

        self._has_prepared = True

        self._prepare_evaluate_members()
        self._prepare_sorting()
        headers = self._prepare_headers()

        if self.model:

            def generate_variables():
                for column in self.bound_columns:
                    if column.query.show:
                        query_kwargs = setdefaults_path(
                            Struct(),
                            column.query,
                            dict(
                                name=column.name,
                                gui__label=column.display_name,
                                attr=column.attr,
                                model=column.table.model,
                            ),
                            {"class": Variable},
                        )
                        yield query_kwargs.pop("class")(**query_kwargs)

            variables = list(generate_variables())

            self.query = Query(
                request=request,
                variables=variables,
                endpoint_dispatch_prefix="__".join(
                    part for part in [self.endpoint_dispatch_prefix, "query"] if part is not None
                ),
                **flatten(self.query_args)
            )
            self.query_form = self.query.form() if self.query.variables else None

            self.query_error = ""
            if self.query_form:
                try:
                    self.data = self.data.filter(self.query.to_q())
                except QueryException as e:
                    self.query_error = str(e)

            def generate_bulk_fields():
                for column in self.bound_columns:
                    if column.bulk.show:
                        bulk_kwargs = setdefaults_path(
                            Struct(),
                            column.bulk,
                            dict(
                                name=column.name,
                                attr=column.attr,
                                required=False,
                                empty_choice_tuple=(None, "", "---", True),
                                model=self.model,
                            ),
                            {"class": Field.from_model},
                        )
                        if bulk_kwargs["class"] == Field.from_model:
                            bulk_kwargs["field_name"] = column.attr
                        yield bulk_kwargs.pop("class")(**bulk_kwargs)

            bulk_fields = list(generate_bulk_fields())

            self.bulk_form = (
                Form(
                    data=request.POST,
                    fields=bulk_fields,
                    endpoint_dispatch_prefix="__".join(
                        part for part in [self.endpoint_dispatch_prefix, "bulk"] if part is not None
                    ),
                    **flatten(self.bulk)
                )
                if bulk_fields
                else None
            )

        self._prepare_auto_rowspan()

        return headers, self.header_levels
Example #3
0
class Table(object):

    """
    Describe a table. Example:

    .. code:: python

        class FooTable(Table):
            class Meta:
                sortable = False
            a = Column()
            b = Column()

    """

    @dispatch(
        column=EMPTY,
        bulk_filter={},
        bulk_exclude={},
        sortable=True,
        attrs=EMPTY,
        attrs__class__listview=True,
        row__attrs__class=EMPTY,
        row__template=None,
        filter__template="tri_query/form.html",
        header__template="tri_table/table_header_rows.html",
        links__template="tri_table/links.html",
        model=None,
        query=EMPTY,
        bulk=EMPTY,
        endpoint_dispatch_prefix=None,
        endpoint__query=lambda table, key, value: table.query.endpoint_dispatch(key=key, value=value)
        if table.query is not None
        else None,
        endpoint__bulk=lambda table, key, value: table.bulk_form.endpoint_dispatch(key=key, value=value)
        if table.bulk is not None
        else None,
        extra=EMPTY,
    )
    def __init__(
        self,
        data=None,
        request=None,
        columns=None,
        columns_dict=None,
        model=None,
        filter=None,
        bulk_exclude=None,
        sortable=None,
        links=None,
        column=None,
        bulk=None,
        header=None,
        bulk_filter=None,
        endpoint=None,
        attrs=None,
        query=None,
        endpoint_dispatch_prefix=None,
        row=None,
        instance=None,
        extra=None,
    ):
        """
        :param data: a list or QuerySet of objects
        :param columns: (use this only when not using the declarative style) a list of Column objects
        :param attrs: dict of strings to string/callable of HTML attributes to apply to the table
        :param row__attrs: dict of strings to string/callable of HTML attributes to apply to the row. Callables are passed the row as argument.
        :param row__template: name of template to use for rendering the row
        :param bulk_filter: filters to apply to the QuerySet before performing the bulk operation
        :param bulk_exclude: exclude filters to apply to the QuerySet before performing the bulk operation
        :param sortable: set this to false to turn off sorting for all columns
        """

        if data is None:  # pragma: no cover
            warnings.warn("deriving model from data queryset is deprecated, use Table.from_model", DeprecationWarning)
            assert model is not None
            data = model.objects.all()

        if isinstance(data, QuerySet):
            model = data.model

        def generate_columns():
            for column_ in columns if columns is not None else []:
                yield column_
            for name, column_ in columns_dict.items():
                dict.__setitem__(column_, "name", name)
                yield column_

        columns = sort_after(list(generate_columns()))

        assert (
            len(columns) > 0
        ), "columns must be specified. It is only set to None to make linting tools not give false positives on the declarative style"

        self.data = data
        self.request = request
        self.columns = columns
        """ :type : list of Column """

        self.model = model
        self.instance = instance

        self.filter = TemplateConfig(**filter)
        self.links = TemplateConfig(**links)
        self.header = TemplateConfig(**header)
        self.row = RowConfig(**row)
        self.bulk_exclude = bulk_exclude
        self.sortable = sortable
        self.column = column
        self.bulk = bulk
        self.bulk_filter = bulk_filter
        self.endpoint = endpoint
        self.endpoint_dispatch_prefix = endpoint_dispatch_prefix
        self.attrs = attrs

        self.query_args = query
        self.query = None
        """ :type : tri.query.Query """
        self.query_form = None
        """ :type : tri.form.Form """
        self.query_error = None
        """ :type : list of str """

        self.bulk_form = None
        """ :type : tri.form.Form """
        self.bound_columns = None
        """ :type : list of BoundColumn """
        self.shown_bound_columns = None
        """ :type : list of BoundColumn """
        self.bound_column_by_name = None
        """ :type: dict[str, BoundColumn] """
        self._has_prepared = False
        """ :type: bool """
        self.header_levels = None

        self.extra = extra
        """ :type: tri.declarative.Namespace """

    def _prepare_auto_rowspan(self):
        auto_rowspan_columns = [column for column in self.shown_bound_columns if column.auto_rowspan]

        if auto_rowspan_columns:
            self.data = list(self.data)
            no_value_set = object()
            for column in auto_rowspan_columns:
                rowspan_by_row = (
                    {}
                )  # cells for rows in this dict are displayed, if they're not in here, they get style="display: none"
                prev_value = no_value_set
                prev_row = no_value_set
                for bound_row in self.bound_rows():
                    value = BoundCell(bound_row, column).value
                    if prev_value != value:
                        rowspan_by_row[id(bound_row.row)] = 1
                        prev_value = value
                        prev_row = bound_row.row
                    else:
                        rowspan_by_row[id(prev_row)] += 1

                column.cell.attrs["rowspan"] = set_row_span(rowspan_by_row)
                assert (
                    "style" not in column.cell.attrs
                )  # TODO: support both specifying style cell__attrs and auto_rowspan
                column.cell.attrs["style"] = set_display_none(rowspan_by_row)

    def _prepare_evaluate_members(self):
        self.shown_bound_columns = [bound_column for bound_column in self.bound_columns if bound_column.show]

        for attr in (
            "column",
            "bulk_filter",
            "bulk_exclude",
            "sortable",
            "attrs",
            "row",
            "filter",
            "header",
            "links",
            "model",
            "query",
            "bulk",
            "endpoint",
        ):
            setattr(self, attr, evaluate_recursive(getattr(self, attr), table=self))

        if not self.sortable:
            for bound_column in self.bound_columns:
                bound_column.sortable = False

    def _prepare_sorting(self):
        # sorting
        order = self.request.GET.get("order", None)
        if order is not None:
            is_desc = order[0] == "-"
            order_field = is_desc and order[1:] or order
            tmp = [x for x in self.shown_bound_columns if x.name == order_field]
            if len(tmp) == 0:
                return  # Unidentified sort column
            sort_column = tmp[0]
            order_args = evaluate(sort_column.sort_key, column=sort_column)
            order_args = isinstance(order_args, list) and order_args or [order_args]

            if sort_column.sortable:
                if isinstance(self.data, list):
                    order_by_on_list(self.data, order_args[0], is_desc)
                else:
                    if not settings.DEBUG:
                        # We should crash on invalid sort commands in DEV, but just ignore in PROD
                        # noinspection PyProtectedMember
                        valid_sort_fields = {x.name for x in self.model._meta.fields}
                        order_args = [
                            order_arg for order_arg in order_args if order_arg.split("__", 1)[0] in valid_sort_fields
                        ]
                    order_args = ["%s%s" % (is_desc and "-" or "", x) for x in order_args]
                    self.data = self.data.order_by(*order_args)

    def _prepare_headers(self):
        bound_columns = prepare_headers(self.request, self.shown_bound_columns)

        # The id(header) and the type(x.display_name) stuff is to make None not be equal to None in the grouping
        group_columns = []

        class GroupColumn(Namespace):
            def render_css_class(self):
                return render_class(self.attrs["class"])

        for group_name, group_iterator in groupby(bound_columns, key=lambda header: header.group or id(header)):

            columns_in_group = list(group_iterator)

            group_columns.append(
                GroupColumn(
                    display_name=group_name,
                    sortable=False,
                    colspan=len(columns_in_group),
                    attrs__class__superheader=True,
                )
            )

            for bound_column in columns_in_group:
                bound_column.attrs["class"].subheader = True
                if bound_column.is_sorting:
                    bound_column.attrs["class"].sorted_column = True

            columns_in_group[0].attrs["class"].first_column = True

        if group_columns:
            group_columns[0].attrs["class"].first_column = True

        for group_column in group_columns:
            if not isinstance(group_column.display_name, string_types):
                group_column.display_name = ""
        if all(c.display_name == "" for c in group_columns):
            group_columns = []

        self.header_levels = [group_columns, bound_columns] if len(group_columns) > 1 else [bound_columns]
        return bound_columns

    # noinspection PyProtectedMember
    def prepare(self, request):
        if self._has_prepared:
            return

        self.request = request

        def bind_columns():
            for index, column in enumerate(self.columns):
                values = evaluate_recursive(Struct(column), table=self, column=column)
                values = setdefaults_path(
                    Struct(), self.column.get(column.name, {}), values, column=column, table=self, index=index
                )
                yield BoundColumn(**values)

        self.bound_columns = list(bind_columns())
        self.bound_column_by_name = OrderedDict(
            (bound_column.name, bound_column) for bound_column in self.bound_columns
        )

        self._has_prepared = True

        self._prepare_evaluate_members()
        self._prepare_sorting()
        headers = self._prepare_headers()

        if self.model:

            def generate_variables():
                for column in self.bound_columns:
                    if column.query.show:
                        query_kwargs = setdefaults_path(
                            Struct(),
                            column.query,
                            dict(
                                name=column.name,
                                gui__label=column.display_name,
                                attr=column.attr,
                                model=column.table.model,
                            ),
                            {"class": Variable},
                        )
                        yield query_kwargs.pop("class")(**query_kwargs)

            variables = list(generate_variables())

            self.query = Query(
                request=request,
                variables=variables,
                endpoint_dispatch_prefix="__".join(
                    part for part in [self.endpoint_dispatch_prefix, "query"] if part is not None
                ),
                **flatten(self.query_args)
            )
            self.query_form = self.query.form() if self.query.variables else None

            self.query_error = ""
            if self.query_form:
                try:
                    self.data = self.data.filter(self.query.to_q())
                except QueryException as e:
                    self.query_error = str(e)

            def generate_bulk_fields():
                for column in self.bound_columns:
                    if column.bulk.show:
                        bulk_kwargs = setdefaults_path(
                            Struct(),
                            column.bulk,
                            dict(
                                name=column.name,
                                attr=column.attr,
                                required=False,
                                empty_choice_tuple=(None, "", "---", True),
                                model=self.model,
                            ),
                            {"class": Field.from_model},
                        )
                        if bulk_kwargs["class"] == Field.from_model:
                            bulk_kwargs["field_name"] = column.attr
                        yield bulk_kwargs.pop("class")(**bulk_kwargs)

            bulk_fields = list(generate_bulk_fields())

            self.bulk_form = (
                Form(
                    data=request.POST,
                    fields=bulk_fields,
                    endpoint_dispatch_prefix="__".join(
                        part for part in [self.endpoint_dispatch_prefix, "bulk"] if part is not None
                    ),
                    **flatten(self.bulk)
                )
                if bulk_fields
                else None
            )

        self._prepare_auto_rowspan()

        return headers, self.header_levels

    def bound_rows(self):
        return self

    def __iter__(self):
        self.prepare(self.request)
        for i, row in enumerate(self.data):
            yield BoundRow(table=self, row=row, row_index=i, **evaluate_recursive(self.row, table=self, row=row))

    def render_attrs(self):
        attrs = self.attrs.copy()
        return render_attrs(attrs)

    def render_tbody(self):
        return "\n".join([bound_row.render() for bound_row in self.bound_rows()])

    @staticmethod
    @dispatch(column=EMPTY)
    def columns_from_model(column, **kwargs):
        return create_members_from_model(
            member_params_by_member_name=column, default_factory=Column.from_model, **kwargs
        )

    @staticmethod
    @dispatch(column=EMPTY)
    def from_model(
        data=None, model=None, column=None, instance=None, include=None, exclude=None, extra_fields=None, **kwargs
    ):
        """
        Create an entire form based on the fields of a model. To override a field parameter send keyword arguments in the form
        of "the_name_of_the_field__param". For example:

        .. code:: python

            class Foo(Model):
                foo = IntegerField()

            Table.from_model(data=request.GET, model=Foo, field__foo__help_text='Overridden help text')

        :param include: fields to include. Defaults to all
        :param exclude: fields to exclude. Defaults to none (except that AutoField is always excluded!)

        """
        assert model or data, "model or data must be specified"
        if model is None and isinstance(data, QuerySet):
            model = data.model
        columns = Table.columns_from_model(
            model=model, include=include, exclude=exclude, extra=extra_fields, column=column
        )
        return Table(data=data, model=model, instance=instance, columns=columns, **kwargs)

    def endpoint_dispatch(self, key, value):
        parts = key.split("__", 1)
        prefix = parts.pop(0)
        remaining_key = parts[0] if parts else None
        for endpoint, handler in self.endpoint.items():
            if prefix == endpoint:
                return handler(table=self, key=remaining_key, value=value)
Example #4
0
    def prepare(self, request):
        if self._has_prepared:
            return

        self.request = request

        def bind_columns():
            for index, column in enumerate(self.columns):
                values = evaluate_recursive(Struct(column), table=self, column=column)
                values = merged(values, column=column, table=self, index=index)
                yield BoundColumn(**values)

        self.bound_columns = list(bind_columns())

        self._has_prepared = True

        self._prepare_evaluate_members()
        self._prepare_sorting()
        headers = self._prepare_headers()
        self._prepare_auto_rowspan()

        if self.Meta.model:
            def bulk(column):
                bulk_kwargs = {
                    'name': column.name,
                    'attr': column.attr,
                    'required': False,
                    'empty_choice_tuple': (None, '', '---', True),
                    'model': self.Meta.model,
                    'class': Field.from_model,
                }
                bulk_kwargs.update(column.bulk)
                if bulk_kwargs['class'] == Field.from_model:
                    bulk_kwargs['field_name'] = column.attr
                return bulk_kwargs.pop('class')(**bulk_kwargs)

            def query(column):
                query_kwargs = {
                    'class': Variable,
                    'name': column.name,
                    'gui__label': column.display_name,
                    'attr': column.attr,
                    'model': column.table.Meta.model,
                }
                query_kwargs.update(column.query)
                return query_kwargs.pop('class')(**query_kwargs)

            self.query = Query(request=request, variables=[query(bound_column) for bound_column in self.bound_columns if bound_column.query.show], **self.query_kwargs)
            self.query_form = self.query.form(request) if self.query.variables else None

            self.query_error = ''
            if self.query_form:
                try:
                    self.data = self.data.filter(self.query.to_q())
                except QueryException as e:
                    self.query_error = e.message

            bulk_fields = [bulk(bound_column) for bound_column in self.bound_columns if bound_column.bulk.show]
            self.bulk_form = Form(data=request.POST, fields=bulk_fields, **self.bulk_kwargs) if bulk_fields else None

        return headers, self.header_levels
Example #5
0
class Table(object):

    """
    Describe a table. Example:

    .. code:: python

        class FooTable(Table):
            class Meta:
                sortable = False
            a = Column()
            b = Column()

    """

    class Meta:
        attrs = {'class': 'listview'}
        bulk_filter = {}
        bulk_exclude = {}
        sortable = True
        row__attrs = {'class': ''}
        row__template = None
        filter__template = 'tri_query/form.html'
        header__template = 'tri_table/table_header_rows.html'
        links__template = 'tri_table/links.html'

        model = None

    def __init__(self, data, request=None, columns=None, **kwargs):
        """
        :param data: a list of QuerySet of objects
        :param columns: (use this only when not using the declarative style) a list of Column objects
        :param attrs: dict of strings to string/callable of HTML attributes to apply to the table
        :param row__attrs: dict of strings to string/callable of HTML attributes to apply to the row. Callables are passed the row as argument.
        :param row__template: name of template to use for rendering the row
        :param bulk_filter: filters to apply to the QuerySet before performing the bulk operation
        :param bulk_exclude: exclude filters to apply to the QuerySet before performing the bulk operation
        :param sortable: set this to false to turn off sorting for all columns
        :param filter__template:
        :param header__template:
        :param links__template:

        """
        assert columns is not None, 'columns must be specified. It is only set to None to make linting tools not give false positives on the declarative style'

        self._has_prepared = False

        self.data = data
        self.request = request

        if isinstance(self.data, QuerySet):
            kwargs['model'] = data.model

        if isinstance(columns, dict):
            for name, column in columns.items():
                dict.__setitem__(column, 'name', name)
            self.columns = columns.values()
        else:
            self.columns = columns
            """:type : list of Column"""

        self.bound_columns = None
        self.shown_bound_columns = None

        self.Meta = self.get_meta()
        self.Meta.update(**kwargs)

        self.header_levels = None
        self.query = None
        self.query_form = None
        self.query_error = None
        self.bulk_form = None

        self.query_kwargs = extract_subkeys(kwargs, 'query')
        self.bulk_kwargs = extract_subkeys(kwargs, 'bulk')

    def _prepare_auto_rowspan(self):
        auto_rowspan_columns = [column for column in self.shown_bound_columns if column.auto_rowspan]

        if auto_rowspan_columns:
            self.data = list(self.data)
            no_value_set = object()
            for column in auto_rowspan_columns:
                rowspan_by_row = {}  # cells for rows in this dict are displayed, if they're not in here, they get style="display: none"
                prev_value = no_value_set
                prev_row = no_value_set
                for bound_row in self.bound_rows():
                    value = column.cell_contents(bound_row=bound_row)
                    if prev_value != value:
                        rowspan_by_row[id(bound_row.row)] = 1
                        prev_value = value
                        prev_row = bound_row.row
                    else:
                        rowspan_by_row[id(prev_row)] += 1

                column.cell__attrs['rowspan'] = set_row_span(rowspan_by_row)
                assert 'style' not in column.cell__attrs  # TODO: support both specifying style cell__attrs and auto_rowspan
                column.cell__attrs['style'] = set_display_none(rowspan_by_row)

    def _prepare_evaluate_members(self):
        self.shown_bound_columns = [bound_column for bound_column in self.bound_columns if bound_column.show]

        model = self.Meta.pop('model')  # avoid trying to eval model, since it's callable
        self.Meta = evaluate_recursive(self.Meta, table=self)
        self.Meta.model = model

        if not self.Meta.sortable:
            for bound_column in self.bound_columns:
                bound_column.sortable = False

    def _prepare_sorting(self):
        # sorting
        order = self.request.GET.get('order', None)
        if order is not None:
            is_desc = order[0] == '-'
            order_field = is_desc and order[1:] or order
            sort_column = [x for x in self.shown_bound_columns if x.name == order_field][0]
            order_args = evaluate(sort_column.sort_key, column=sort_column)
            order_args = isinstance(order_args, list) and order_args or [order_args]

            if sort_column.sortable:
                if isinstance(self.data, list):
                    order_by_on_list(self.data, order_args[0], is_desc)
                else:
                    if not settings.DEBUG:
                        # We should crash on invalid sort commands in DEV, but just ignore in PROD
                        # noinspection PyProtectedMember
                        valid_sort_fields = {x.name for x in self.Meta.model._meta.fields}
                        order_args = [order_arg for order_arg in order_args if order_arg.split('__', 1)[0] in valid_sort_fields]
                    order_args = ["%s%s" % (is_desc and '-' or '', x) for x in order_args]
                    self.data = self.data.order_by(*order_args)

    def _prepare_headers(self):
        headers = prepare_headers(self.request, self.shown_bound_columns)

        # The id(header) and the type(x.display_name) stuff is to make None not be equal to None in the grouping
        header_groups = []

        class HeaderGroup(Struct):
            def render_css_class(self):
                return ' '.join(sorted(self.css_class))

        for group_name, group_iterator in groupby(headers, key=lambda header: header.group or id(header)):

            header_group = list(group_iterator)

            header_groups.append(HeaderGroup(
                display_name=group_name,
                sortable=False,
                colspan=len(header_group),
                css_class={'superheader'}))

            for x in header_group:
                x.css_class.add('subheader')
                if x.is_sorting:
                    x.css_class.add('sorted_column')

            header_group[0].css_class.add('first_column')

        header_groups[0].css_class.add('first_column')

        for x in header_groups:
            if type(x.display_name) not in (str, unicode):
                x.display_name = ''
        if all([x.display_name == '' for x in header_groups]):
            header_groups = []

        self.header_levels = [header_groups, headers] if len(header_groups) > 1 else [headers]
        return headers

    # noinspection PyProtectedMember
    def prepare(self, request):
        if self._has_prepared:
            return

        self.request = request

        def bind_columns():
            for index, column in enumerate(self.columns):
                values = evaluate_recursive(Struct(column), table=self, column=column)
                values = merged(values, column=column, table=self, index=index)
                yield BoundColumn(**values)

        self.bound_columns = list(bind_columns())

        self._has_prepared = True

        self._prepare_evaluate_members()
        self._prepare_sorting()
        headers = self._prepare_headers()
        self._prepare_auto_rowspan()

        if self.Meta.model:
            def bulk(column):
                bulk_kwargs = {
                    'name': column.name,
                    'attr': column.attr,
                    'required': False,
                    'empty_choice_tuple': (None, '', '---', True),
                    'model': self.Meta.model,
                    'class': Field.from_model,
                }
                bulk_kwargs.update(column.bulk)
                if bulk_kwargs['class'] == Field.from_model:
                    bulk_kwargs['field_name'] = column.attr
                return bulk_kwargs.pop('class')(**bulk_kwargs)

            def query(column):
                query_kwargs = {
                    'class': Variable,
                    'name': column.name,
                    'gui__label': column.display_name,
                    'attr': column.attr,
                    'model': column.table.Meta.model,
                }
                query_kwargs.update(column.query)
                return query_kwargs.pop('class')(**query_kwargs)

            self.query = Query(request=request, variables=[query(bound_column) for bound_column in self.bound_columns if bound_column.query.show], **self.query_kwargs)
            self.query_form = self.query.form(request) if self.query.variables else None

            self.query_error = ''
            if self.query_form:
                try:
                    self.data = self.data.filter(self.query.to_q())
                except QueryException as e:
                    self.query_error = e.message

            bulk_fields = [bulk(bound_column) for bound_column in self.bound_columns if bound_column.bulk.show]
            self.bulk_form = Form(data=request.POST, fields=bulk_fields, **self.bulk_kwargs) if bulk_fields else None

        return headers, self.header_levels

    def bound_rows(self):
        row_params = extract_subkeys(self.Meta, 'row')
        for i, row in enumerate(self.data):
            yield BoundRow(table=self, row=row, row_index=i, **row_params)

    def render_attrs(self):
        return render_attrs(self.Meta.attrs)

    def render_tbody(self):
        return '\n'.join([bound_row.render() for bound_row in self.bound_rows()])
Example #6
0
def test_none_attr():
    query2 = Query(request=Struct(method='GET', GET=Data(**{'bazaar': 'foo'})), variables=[Variable(name='bazaar', attr=None, gui__show=True)])
    # noinspection PyTypeChecker
    assert repr(query2.to_q()) == repr(Q())
Example #7
0
def test_invalid_form_data():
    query2 = Query(request=Struct(method='GET', GET=Data(**{'bazaar': 'asds'})), variables=[Variable.integer(name='bazaar', attr='quux__bar__bazaar', gui__show=True)])
    # noinspection PyTypeChecker
    assert query2.to_query_string() == ''
    # noinspection PyTypeChecker
    assert repr(query2.to_q()) == repr(Q())
Example #8
0
def test_invalid_variable():
    query2 = Query(request=Struct(method='GET', GET=Data(**{'query': 'not_bazaar=asd'})), variables=[Variable(name='bazaar')])
    with pytest.raises(QueryException) as e:
        query2.to_q()
    assert e.value.message == 'Unknown variable "not_bazaar"'
Example #9
0
def test_from_model():
    t = Query.from_model(data=Foo.objects.all(), model=Foo)
    assert [x.name for x in t.variables] == ['id', 'value']
    assert [x.name for x in t.variables if x.show] == ['value']
Example #10
0
def test_invalid_variable():
    # noinspection PyTypeChecker
    query2 = Query(request=Struct(method='GET', GET=Data(**{'query': 'not_bazaar=asd'})), variables=[Variable(name='bazaar')])
    with pytest.raises(QueryException) as e:
        query2.to_q()
    assert 'Unknown variable "not_bazaar"' in str(e)
Example #11
0
    def prepare(self, request):
        if self._has_prepared:
            return

        self.request = request

        def bind_columns():
            for index, column in enumerate(self.columns):
                values = evaluate_recursive(Struct(column),
                                            table=self,
                                            column=column)
                values = merged(values, column=column, table=self, index=index)
                yield BoundColumn(**values)

        self.bound_columns = list(bind_columns())
        self.bound_column_by_name = OrderedDict(
            (bound_column.name, bound_column)
            for bound_column in self.bound_columns)

        self._has_prepared = True

        self._prepare_evaluate_members()
        self._prepare_sorting()
        headers = self._prepare_headers()

        if self.Meta.model:

            def generate_variables():
                for column in self.bound_columns:
                    if column.query.show:
                        query_kwargs = setdefaults_path(
                            Struct(), column.query,
                            dict(
                                name=column.name,
                                gui__label=column.display_name,
                                attr=column.attr,
                                model=column.table.Meta.model,
                            ), {
                                'class': Variable,
                            })
                        yield query_kwargs.pop('class')(**query_kwargs)

            variables = list(generate_variables())

            self.query = Query(request=request,
                               variables=variables,
                               **self.query_kwargs)
            self.query_form = self.query.form(
            ) if self.query.variables else None

            self.query_error = ''
            if self.query_form:
                try:
                    self.data = self.data.filter(self.query.to_q())
                except QueryException as e:
                    self.query_error = str(e)

            def generate_bulk_fields():
                for column in self.bound_columns:
                    if column.bulk.show:
                        bulk_kwargs = setdefaults_path(
                            Struct(), column.bulk,
                            dict(
                                name=column.name,
                                attr=column.attr,
                                required=False,
                                empty_choice_tuple=(None, '', '---', True),
                                model=self.Meta.model,
                            ), {
                                'class': Field.from_model,
                            })
                        if bulk_kwargs['class'] == Field.from_model:
                            bulk_kwargs['field_name'] = column.attr
                        yield bulk_kwargs.pop('class')(**bulk_kwargs)

            bulk_fields = list(generate_bulk_fields())

            self.bulk_form = Form(data=request.POST,
                                  fields=bulk_fields,
                                  endpoint_dispatch_prefix='bulk',
                                  **self.bulk_kwargs) if bulk_fields else None

        self._prepare_auto_rowspan()

        return headers, self.header_levels
Example #12
0
class Table(object):
    """
    Describe a table. Example:

    .. code:: python

        class FooTable(Table):
            class Meta:
                sortable = False
            a = Column()
            b = Column()

    """
    class Meta:
        bulk_filter = {}
        bulk_exclude = {}
        sortable = True
        attrs = Struct()
        attrs__class__listview = True
        row__attrs = Struct()
        row__template = None
        filter__template = 'tri_query/form.html'
        header__template = 'tri_table/table_header_rows.html'
        links__template = 'tri_table/links.html'

        endpoint__query__ = lambda table, key, value: table.query.endpoint_dispatch(
            key=key, value=value) if table.query is not None else None
        endpoint__bulk__ = lambda table, key, value: table.bulk.endpoint_dispatch(
            key=key, value=value) if table.bulk is not None else None

        model = None

    def __init__(self,
                 data=None,
                 request=None,
                 columns=None,
                 columns_dict=None,
                 **kwargs):
        """
        :param data: a list of QuerySet of objects
        :param columns: (use this only when not using the declarative style) a list of Column objects
        :param attrs: dict of strings to string/callable of HTML attributes to apply to the table
        :param row__attrs: dict of strings to string/callable of HTML attributes to apply to the row. Callables are passed the row as argument.
        :param row__template: name of template to use for rendering the row
        :param bulk_filter: filters to apply to the QuerySet before performing the bulk operation
        :param bulk_exclude: exclude filters to apply to the QuerySet before performing the bulk operation
        :param sortable: set this to false to turn off sorting for all columns
        :param filter__template:
        :param header__template:
        :param links__template:

        """

        self._has_prepared = False

        if data is None:
            assert 'model' in kwargs and kwargs['model'] is not None
            data = kwargs['model'].objects.all()

        self.data = data
        self.request = request

        if isinstance(self.data, QuerySet):
            kwargs['model'] = data.model

        def generate_columns():
            for column in columns if columns is not None else []:
                yield column
            for name, column in columns_dict.items():
                dict.__setitem__(column, 'name', name)
                yield column

        self.columns = sort_after(list(generate_columns()))
        """:type : list of Column"""

        assert len(
            self.columns
        ) > 0, 'columns must be specified. It is only set to None to make linting tools not give false positives on the declarative style'

        self.bound_columns = None
        self.shown_bound_columns = None
        self.bound_column_by_name = None

        self.Meta = self.get_meta()
        self.Meta.update(**kwargs)

        self.header_levels = None
        self.query = None
        self.query_form = None
        self.query_error = None
        self.bulk_form = None

        self.query_kwargs = extract_subkeys(kwargs, 'query')
        self.bulk_kwargs = extract_subkeys(kwargs, 'bulk')
        self.endpoint = extract_subkeys(kwargs, 'endpoint')

    def _prepare_auto_rowspan(self):
        auto_rowspan_columns = [
            column for column in self.shown_bound_columns
            if column.auto_rowspan
        ]

        if auto_rowspan_columns:
            self.data = list(self.data)
            no_value_set = object()
            for column in auto_rowspan_columns:
                rowspan_by_row = {
                }  # cells for rows in this dict are displayed, if they're not in here, they get style="display: none"
                prev_value = no_value_set
                prev_row = no_value_set
                for bound_row in self.bound_rows():
                    value = BoundCell(bound_row, column).value
                    if prev_value != value:
                        rowspan_by_row[id(bound_row.row)] = 1
                        prev_value = value
                        prev_row = bound_row.row
                    else:
                        rowspan_by_row[id(prev_row)] += 1

                column.cell.attrs['rowspan'] = set_row_span(rowspan_by_row)
                assert 'style' not in column.cell.attrs  # TODO: support both specifying style cell__attrs and auto_rowspan
                column.cell.attrs['style'] = set_display_none(rowspan_by_row)

    def _prepare_evaluate_members(self):
        self.shown_bound_columns = [
            bound_column for bound_column in self.bound_columns
            if bound_column.show
        ]

        self.Meta = evaluate_recursive(self.Meta, table=self)

        if 'class' in self.Meta.attrs and isinstance(self.Meta.attrs['class'],
                                                     string_types):
            self.Meta.attrs['class'] = {
                k: True
                for k in self.Meta.attrs['class'].split(' ')
            }
        else:
            self.Meta.attrs['class'] = {}
        self.Meta.attrs.update(extract_subkeys(self.Meta, 'attrs'))
        self.Meta.attrs = collect_namespaces(self.Meta.attrs)

        if 'class' in self.Meta.row__attrs and isinstance(
                self.Meta.row__attrs['class'], string_types):
            self.Meta.row__attrs['class'] = {
                k: True
                for k in self.Meta.row__attrs['class'].split(' ')
            }
        else:
            self.Meta.row__attrs['class'] = {}
        self.Meta.row__attrs.update(extract_subkeys(self.Meta, 'row__attrs'))
        self.Meta.row__attrs = collect_namespaces(self.Meta.row__attrs)

        if not self.Meta.sortable:
            for bound_column in self.bound_columns:
                bound_column.sortable = False

    def _prepare_sorting(self):
        # sorting
        order = self.request.GET.get('order', None)
        if order is not None:
            is_desc = order[0] == '-'
            order_field = is_desc and order[1:] or order
            sort_column = [
                x for x in self.shown_bound_columns if x.name == order_field
            ][0]
            order_args = evaluate(sort_column.sort_key, column=sort_column)
            order_args = isinstance(order_args, list) and order_args or [
                order_args
            ]

            if sort_column.sortable:
                if isinstance(self.data, list):
                    order_by_on_list(self.data, order_args[0], is_desc)
                else:
                    if not settings.DEBUG:
                        # We should crash on invalid sort commands in DEV, but just ignore in PROD
                        # noinspection PyProtectedMember
                        valid_sort_fields = {
                            x.name
                            for x in self.Meta.model._meta.fields
                        }
                        order_args = [
                            order_arg for order_arg in order_args
                            if order_arg.split('__', 1)[0] in valid_sort_fields
                        ]
                    order_args = [
                        "%s%s" % (is_desc and '-' or '', x) for x in order_args
                    ]
                    self.data = self.data.order_by(*order_args)

    def _prepare_headers(self):
        headers = prepare_headers(self.request, self.shown_bound_columns)

        # The id(header) and the type(x.display_name) stuff is to make None not be equal to None in the grouping
        header_groups = []

        class HeaderGroup(Struct):
            def render_css_class(self):
                return render_class(self.attrs['class'])

        for group_name, group_iterator in groupby(
                headers, key=lambda header: header.group or id(header)):

            header_group = list(group_iterator)

            header_groups.append(
                HeaderGroup(display_name=group_name,
                            sortable=False,
                            colspan=len(header_group),
                            attrs=Struct({'class': Struct(superheader=True)})))

            for x in header_group:
                x.attrs['class']['subheader'] = True
                if x.is_sorting:
                    x.attrs['class']['sorted_column'] = True

            header_group[0].attrs['class']['first_column'] = True

        if header_groups:
            header_groups[0].attrs['class']['first_column'] = True

        for x in header_groups:
            if not isinstance(x.display_name, string_types):
                x.display_name = ''
        if all([x.display_name == '' for x in header_groups]):
            header_groups = []

        self.header_levels = [header_groups, headers
                              ] if len(header_groups) > 1 else [headers]
        return headers

    # noinspection PyProtectedMember
    def prepare(self, request):
        if self._has_prepared:
            return

        self.request = request

        def bind_columns():
            for index, column in enumerate(self.columns):
                values = evaluate_recursive(Struct(column),
                                            table=self,
                                            column=column)
                values = merged(values, column=column, table=self, index=index)
                yield BoundColumn(**values)

        self.bound_columns = list(bind_columns())
        self.bound_column_by_name = OrderedDict(
            (bound_column.name, bound_column)
            for bound_column in self.bound_columns)

        self._has_prepared = True

        self._prepare_evaluate_members()
        self._prepare_sorting()
        headers = self._prepare_headers()

        if self.Meta.model:

            def generate_variables():
                for column in self.bound_columns:
                    if column.query.show:
                        query_kwargs = setdefaults_path(
                            Struct(), column.query,
                            dict(
                                name=column.name,
                                gui__label=column.display_name,
                                attr=column.attr,
                                model=column.table.Meta.model,
                            ), {
                                'class': Variable,
                            })
                        yield query_kwargs.pop('class')(**query_kwargs)

            variables = list(generate_variables())

            self.query = Query(request=request,
                               variables=variables,
                               **self.query_kwargs)
            self.query_form = self.query.form(
            ) if self.query.variables else None

            self.query_error = ''
            if self.query_form:
                try:
                    self.data = self.data.filter(self.query.to_q())
                except QueryException as e:
                    self.query_error = str(e)

            def generate_bulk_fields():
                for column in self.bound_columns:
                    if column.bulk.show:
                        bulk_kwargs = setdefaults_path(
                            Struct(), column.bulk,
                            dict(
                                name=column.name,
                                attr=column.attr,
                                required=False,
                                empty_choice_tuple=(None, '', '---', True),
                                model=self.Meta.model,
                            ), {
                                'class': Field.from_model,
                            })
                        if bulk_kwargs['class'] == Field.from_model:
                            bulk_kwargs['field_name'] = column.attr
                        yield bulk_kwargs.pop('class')(**bulk_kwargs)

            bulk_fields = list(generate_bulk_fields())

            self.bulk_form = Form(data=request.POST,
                                  fields=bulk_fields,
                                  endpoint_dispatch_prefix='bulk',
                                  **self.bulk_kwargs) if bulk_fields else None

        self._prepare_auto_rowspan()

        return headers, self.header_levels

    def bound_rows(self):
        return self

    def __iter__(self):
        self.prepare(self.request)
        for i, row in enumerate(self.data):
            yield BoundRow(table=self, row=row, row_index=i)

    def render_attrs(self):
        attrs = self.Meta.attrs.copy()
        return render_attrs(attrs)

    def render_tbody(self):
        return '\n'.join(
            [bound_row.render() for bound_row in self.bound_rows()])

    @staticmethod
    def columns_from_model(**kwargs):
        kwargs = collect_namespaces(kwargs)
        kwargs['db_field'] = collect_namespaces(kwargs.pop('column', {}))
        return create_members_from_model(default_factory=Column.from_model,
                                         **kwargs)

    @staticmethod
    def from_model(data,
                   model,
                   instance=None,
                   include=None,
                   exclude=None,
                   extra_fields=None,
                   post_validation=None,
                   **kwargs):
        """
        Create an entire form based on the fields of a model. To override a field parameter send keyword arguments in the form
        of "the_name_of_the_field__param". For example:

        .. code:: python

            class Foo(Model):
                foo = IntegerField()

            Table.from_model(data=request.GET, model=Foo, field__foo__help_text='Overridden help text')

        :param include: fields to include. Defaults to all
        :param exclude: fields to exclude. Defaults to none (except that AutoField is always excluded!)

        """
        kwargs = collect_namespaces(kwargs)
        columns = Table.columns_from_model(model=model,
                                           include=include,
                                           exclude=exclude,
                                           extra=extra_fields,
                                           column=kwargs.pop('column', {}))
        return Table(data=data,
                     model=model,
                     instance=instance,
                     columns=columns,
                     post_validation=post_validation,
                     **kwargs)

    def endpoint_dispatch(self, key, value):
        for endpoint, handler in self.endpoint.items():
            if key.startswith(endpoint):
                return handler(table=self,
                               key=key[len(endpoint):],
                               value=value)