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)
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
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)
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
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()])
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())
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())
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"'
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']
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)
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
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)