-from django.core.exceptions import FieldError\r
-from django.utils.datastructures import SortedDict\r
-from base import BaseTable, DeclarativeColumnsMetaclass, \\r
- Column, BoundRow, Rows, TableOptions, rmprefix, toggleprefix\r
-\r
-\r
-__all__ = ('ModelTable',)\r
-\r
-\r
-class ModelTableOptions(TableOptions):\r
- def __init__(self, options=None):\r
- super(ModelTableOptions, self).__init__(options)\r
- self.model = getattr(options, 'model', None)\r
- self.columns = getattr(options, 'columns', None)\r
- self.exclude = getattr(options, 'exclude', None)\r
-\r
-\r
-def columns_for_model(model, columns=None, exclude=None):\r
- """\r
- Returns a ``SortedDict`` containing form columns for the given model.\r
-\r
- ``columns`` is an optional list of field names. If provided, only the\r
- named model fields will be included in the returned column list.\r
-\r
- ``exclude`` is an optional list of field names. If provided, the named\r
- model fields will be excluded from the returned list of columns, even\r
- if they are listed in the ``fields`` argument.\r
- """\r
-\r
- field_list = []\r
- opts = model._meta\r
- for f in opts.fields + opts.many_to_many:\r
- if (columns and not f.name in columns) or \\r
- (exclude and f.name in exclude):\r
- continue\r
- column = Column() # TODO: chose correct column type, with right options\r
- if column:\r
- field_list.append((f.name, column))\r
- return SortedDict(field_list)\r
-\r
-\r
-class BoundModelRow(BoundRow):\r
- """Special version of the BoundRow class that can handle model instances\r
- as data.\r
-\r
- We could simply have ModelTable spawn the normal BoundRow objects\r
- with the instance converted to a dict instead. However, this way allows\r
- us to support non-field attributes and methods on the model as well.\r
- """\r
-\r
- def _default_render(self, boundcol):\r
- """In the case of a model table, the accessor may use ``__`` to\r
- span instances. We need to resolve this.\r
- """\r
- # try to resolve relationships spanning attributes\r
- bits = boundcol.accessor.split('__')\r
- current = self.data\r
- for bit in bits:\r
- # note the difference between the attribute being None and not\r
- # existing at all; assume "value doesn't exist" in the former\r
- # (e.g. a relationship has no value), raise error in the latter.\r
- # a more proper solution perhaps would look at the model meta\r
- # data instead to find out whether a relationship is valid; see\r
- # also ``_validate_column_name``, where such a mechanism is\r
- # already implemented).\r
- if not hasattr(current, bit):\r
- raise ValueError("Could not resolve %s from %s" % (bit, name))\r
-\r
- current = getattr(current, bit)\r
- if callable(current):\r
- current = current()\r
- # important that we break in None case, or a relationship\r
- # spanning across a null-key will raise an exception in the\r
- # next iteration, instead of defaulting.\r
- if current is None:\r
- break\r
-\r
- if current is None:\r
- # ...the whole name (i.e. the last bit) resulted in None\r
- if boundcol.column.default is not None:\r
- return boundcol.get_default(self)\r
- return current\r
-\r
-\r
-class ModelRows(Rows):\r
- row_class = BoundModelRow\r
-\r
- def __init__(self, *args, **kwargs):\r
- super(ModelRows, self).__init__(*args, **kwargs)\r
-\r
- def _reset(self):\r
- self._length = None\r
-\r
- def __len__(self):\r
- """Use the queryset count() method to get the length, instead of\r
- loading all results into memory. This allows, for example,\r
- smart paginators that use len() to perform better.\r
- """\r
- if getattr(self, '_length', None) is None:\r
- self._length = self.table.data.count()\r
- return self._length\r
-\r
- # for compatibility with QuerySetPaginator\r
- count = __len__\r
-\r
-\r
-class ModelTableMetaclass(DeclarativeColumnsMetaclass):\r
- def __new__(cls, name, bases, attrs):\r
- # Let the default form meta class get the declared columns; store\r
- # those in a separate attribute so that ModelTable inheritance with\r
- # differing models works as expected (the behaviour known from\r
- # ModelForms).\r
- self = super(ModelTableMetaclass, cls).__new__(\r
- cls, name, bases, attrs, parent_cols_from='declared_columns')\r
- self.declared_columns = self.base_columns\r
-\r
- opts = self._meta = ModelTableOptions(getattr(self, 'Meta', None))\r
- # if a model is defined, then build a list of default columns and\r
- # let the declared columns override them.\r
- if opts.model:\r
- columns = columns_for_model(opts.model, opts.columns, opts.exclude)\r
- columns.update(self.declared_columns)\r
- self.base_columns = columns\r
- return self\r
-\r
-\r
-class ModelTable(BaseTable):\r
- """Table that is based on a model.\r
-\r
- Similar to ModelForm, a column will automatically be created for all\r
- the model's fields. You can modify this behaviour with a inner Meta\r
- class:\r
-\r
- class MyTable(ModelTable):\r
- class Meta:\r
- model = MyModel\r
- exclude = ['fields', 'to', 'exclude']\r
- columns = ['fields', 'to', 'include']\r
-\r
- One difference to a normal table is the initial data argument. It can\r
- be a queryset or a model (it's default manager will be used). If you\r
- just don't any data at all, the model the table is based on will\r
- provide it.\r
- """\r
-\r
- __metaclass__ = ModelTableMetaclass\r
-\r
- rows_class = ModelRows\r
-\r
- def __init__(self, data=None, *args, **kwargs):\r
- if data == None:\r
- if self._meta.model is None:\r
- raise ValueError('Table without a model association needs '\r
- 'to be initialized with data')\r
- self.queryset = self._meta.model._default_manager.all()\r
- elif hasattr(data, '_default_manager'): # saves us db.models import\r
- self.queryset = data._default_manager.all()\r
- else:\r
- self.queryset = data\r
-\r
- super(ModelTable, self).__init__(self.queryset, *args, **kwargs)\r
-\r
- def _validate_column_name(self, name, purpose):\r
- """Overridden. Only allow model-based fields and valid model\r
- spanning relationships to be sorted."""\r
-\r
- # let the base class sort out the easy ones\r
- result = super(ModelTable, self)._validate_column_name(name, purpose)\r
- if not result:\r
- return False\r
-\r
- if purpose == 'order_by':\r
- column = self.columns[name]\r
-\r
- # "data" can really be used in two different ways. It is\r
- # slightly confusing and potentially should be changed.\r
- # It can either refer to an attribute/field which the table\r
- # column should represent, or can be a callable (or a string\r
- # pointing to a callable attribute) that is used to render to\r
- # cell. The difference is that in the latter case, there may\r
- # still be an actual source model field behind the column,\r
- # stored in "declared_name". In other words, we want to filter\r
- # out column names that are not oderable, and the column name\r
- # we need to check may either be stored in "data" or in\r
- # "declared_name", depending on if and what kind of value is\r
- # in "data". This is the reason why we try twice.\r
- #\r
- # See also bug #282964.\r
- #\r
- # TODO: It might be faster to try to resolve the given name\r
- # manually recursing the model metadata rather than\r
- # constructing a queryset.\r
- for lookup in (column.column.data, column.declared_name):\r
- if not lookup or callable(lookup):\r
- continue\r
- try:\r
- # Let Django validate the lookup by asking it to build\r
- # the final query; the way to do this has changed in\r
- # Django 1.2, and we try to support both versions.\r
- _temp = self.queryset.order_by(lookup).query\r
- if hasattr(_temp, 'as_sql'):\r
- _temp.as_sql()\r
- else:\r
- from django.db import DEFAULT_DB_ALIAS\r
- _temp.get_compiler(DEFAULT_DB_ALIAS).as_sql()\r
- break\r
- except FieldError:\r
- pass\r
- else:\r
- return False\r
-\r
- # if we haven't failed by now, the column should be valid\r
- return True\r
-\r
- def _build_snapshot(self):\r
- """Overridden. The snapshot in this case is simply a queryset\r
- with the necessary filters etc. attached.\r
- """\r
-\r
- # reset caches\r
- self._columns._reset()\r
- self._rows._reset()\r
-\r
- queryset = self.queryset\r
- if self.order_by:\r
- actual_order_by = self._resolve_sort_directions(self.order_by)\r
- queryset = queryset.order_by(*self._cols_to_fields(actual_order_by))\r
- return queryset\r
+from django.core.exceptions import FieldError
+from django.utils.datastructures import SortedDict
+from base import BaseTable, DeclarativeColumnsMetaclass, \
+ Column, BoundRow, Rows, TableOptions, rmprefix, toggleprefix
+
+
+__all__ = ('ModelTable',)
+
+
+class ModelTableOptions(TableOptions):
+ def __init__(self, options=None):
+ super(ModelTableOptions, self).__init__(options)
+ self.model = getattr(options, 'model', None)
+ self.columns = getattr(options, 'columns', None)
+ self.exclude = getattr(options, 'exclude', None)
+
+
+def columns_for_model(model, columns=None, exclude=None):
+ """
+ Returns a ``SortedDict`` containing form columns for the given model.
+
+ ``columns`` is an optional list of field names. If provided, only the
+ named model fields will be included in the returned column list.
+
+ ``exclude`` is an optional list of field names. If provided, the named
+ model fields will be excluded from the returned list of columns, even
+ if they are listed in the ``fields`` argument.
+ """
+
+ field_list = []
+ opts = model._meta
+ for f in opts.fields + opts.many_to_many:
+ if (columns and not f.name in columns) or \
+ (exclude and f.name in exclude):
+ continue
+ column = Column() # TODO: chose correct column type, with right options
+ if column:
+ field_list.append((f.name, column))
+ field_dict = SortedDict(field_list)
+ if columns:
+ field_dict = SortedDict(
+ [(c, field_dict.get(c)) for c in columns
+ if ((not exclude) or (exclude and c not in exclude))]
+ )
+ return field_dict
+
+
+class BoundModelRow(BoundRow):
+ """Special version of the BoundRow class that can handle model instances
+ as data.
+
+ We could simply have ModelTable spawn the normal BoundRow objects
+ with the instance converted to a dict instead. However, this way allows
+ us to support non-field attributes and methods on the model as well.
+ """
+
+ def _default_render(self, boundcol):
+ """In the case of a model table, the accessor may use ``__`` to
+ span instances. We need to resolve this.
+ """
+ # try to resolve relationships spanning attributes
+ bits = boundcol.accessor.split('__')
+ current = self.data
+ for bit in bits:
+ # note the difference between the attribute being None and not
+ # existing at all; assume "value doesn't exist" in the former
+ # (e.g. a relationship has no value), raise error in the latter.
+ # a more proper solution perhaps would look at the model meta
+ # data instead to find out whether a relationship is valid; see
+ # also ``_validate_column_name``, where such a mechanism is
+ # already implemented).
+ if not hasattr(current, bit):
+ raise ValueError("Could not resolve %s from %s" % (bit, name))
+
+ current = getattr(current, bit)
+ if callable(current):
+ current = current()
+ # important that we break in None case, or a relationship
+ # spanning across a null-key will raise an exception in the
+ # next iteration, instead of defaulting.
+ if current is None:
+ break
+
+ if current is None:
+ # ...the whole name (i.e. the last bit) resulted in None
+ if boundcol.column.default is not None:
+ return boundcol.get_default(self)
+ return current
+
+
+class ModelRows(Rows):
+ row_class = BoundModelRow
+
+ def __init__(self, *args, **kwargs):
+ super(ModelRows, self).__init__(*args, **kwargs)
+
+ def _reset(self):
+ self._length = None
+
+ def __len__(self):
+ """Use the queryset count() method to get the length, instead of
+ loading all results into memory. This allows, for example,
+ smart paginators that use len() to perform better.
+ """
+ if getattr(self, '_length', None) is None:
+ self._length = self.table.data.count()
+ return self._length
+
+ # for compatibility with QuerySetPaginator
+ count = __len__
+
+
+class ModelTableMetaclass(DeclarativeColumnsMetaclass):
+ def __new__(cls, name, bases, attrs):
+ # Let the default form meta class get the declared columns; store
+ # those in a separate attribute so that ModelTable inheritance with
+ # differing models works as expected (the behaviour known from
+ # ModelForms).
+ self = super(ModelTableMetaclass, cls).__new__(
+ cls, name, bases, attrs, parent_cols_from='declared_columns')
+ self.declared_columns = self.base_columns
+
+ opts = self._meta = ModelTableOptions(getattr(self, 'Meta', None))
+ # if a model is defined, then build a list of default columns and
+ # let the declared columns override them.
+ if opts.model:
+ columns = columns_for_model(opts.model, opts.columns, opts.exclude)
+ columns.update(self.declared_columns)
+ self.base_columns = columns
+ return self
+
+
+class ModelTable(BaseTable):
+ """Table that is based on a model.
+
+ Similar to ModelForm, a column will automatically be created for all
+ the model's fields. You can modify this behaviour with a inner Meta
+ class:
+
+ class MyTable(ModelTable):
+ class Meta:
+ model = MyModel
+ exclude = ['fields', 'to', 'exclude']
+ columns = ['fields', 'to', 'include']
+
+ One difference to a normal table is the initial data argument. It can
+ be a queryset or a model (it's default manager will be used). If you
+ just don't any data at all, the model the table is based on will
+ provide it.
+ """
+
+ __metaclass__ = ModelTableMetaclass
+
+ rows_class = ModelRows
+
+ def __init__(self, data=None, *args, **kwargs):
+ if data == None:
+ if self._meta.model is None:
+ raise ValueError('Table without a model association needs '
+ 'to be initialized with data')
+ self.queryset = self._meta.model._default_manager.all()
+ elif hasattr(data, '_default_manager'): # saves us db.models import
+ self.queryset = data._default_manager.all()
+ else:
+ self.queryset = data
+
+ super(ModelTable, self).__init__(self.queryset, *args, **kwargs)
+
+ def _validate_column_name(self, name, purpose):
+ """Overridden. Only allow model-based fields and valid model
+ spanning relationships to be sorted."""
+
+ # let the base class sort out the easy ones
+ result = super(ModelTable, self)._validate_column_name(name, purpose)
+ if not result:
+ return False
+
+ if purpose == 'order_by':
+ column = self.columns[name]
+
+ # "data" can really be used in two different ways. It is
+ # slightly confusing and potentially should be changed.
+ # It can either refer to an attribute/field which the table
+ # column should represent, or can be a callable (or a string
+ # pointing to a callable attribute) that is used to render to
+ # cell. The difference is that in the latter case, there may
+ # still be an actual source model field behind the column,
+ # stored in "declared_name". In other words, we want to filter
+ # out column names that are not oderable, and the column name
+ # we need to check may either be stored in "data" or in
+ # "declared_name", depending on if and what kind of value is
+ # in "data". This is the reason why we try twice.
+ #
+ # See also bug #282964.
+ #
+ # TODO: It might be faster to try to resolve the given name
+ # manually recursing the model metadata rather than
+ # constructing a queryset.
+ for lookup in (column.column.data, column.declared_name):
+ if not lookup or callable(lookup):
+ continue
+ try:
+ # Let Django validate the lookup by asking it to build
+ # the final query; the way to do this has changed in
+ # Django 1.2, and we try to support both versions.
+ _temp = self.queryset.order_by(lookup).query
+ if hasattr(_temp, 'as_sql'):
+ _temp.as_sql()
+ else:
+ from django.db import DEFAULT_DB_ALIAS
+ _temp.get_compiler(DEFAULT_DB_ALIAS).as_sql()
+ break
+ except FieldError:
+ pass
+ else:
+ return False
+
+ # if we haven't failed by now, the column should be valid
+ return True
+
+ def _build_snapshot(self):
+ """Overridden. The snapshot in this case is simply a queryset
+ with the necessary filters etc. attached.
+ """
+
+ # reset caches
+ self._columns._reset()
+ self._rows._reset()
+
+ queryset = self.queryset
+ if self.order_by:
+ actual_order_by = self._resolve_sort_directions(self.order_by)
+ queryset = queryset.order_by(*self._cols_to_fields(actual_order_by))
+ return queryset