--- /dev/null
+===============\r
+Ordering Syntax\r
+===============\r
+\r
+Works exactly like in the Django database API. Order may be specified as\r
+a list (or tuple) of column names. If prefixed with a hypen, the ordering\r
+for that particular field will be in reverse order.\r
+\r
+Random ordering is currently not supported.\r
+\r
+====\r
+TODO\r
+====\r
+\r
+ - Support table filters\r
+ - Support grouping\r
+ - Support choices-like data
\ No newline at end of file
--- /dev/null
+from tables import *\r
+from models import *\r
+from columns import *
\ No newline at end of file
--- /dev/null
+__all__ = (\r
+ 'Column',\r
+)\r
+\r
+class Column(object):\r
+ """Represents a single column of a table.\r
+\r
+ ``verbose_name`` defines a display name for this column used for output.\r
+\r
+ ``name`` is the internal name of the column. Normally you don't need to\r
+ specify this, as the attribute that you make the column available under\r
+ is used. However, in certain circumstances it can be useful to override\r
+ this default, e.g. when using ModelTables if you want a column to not\r
+ use the model field name.\r
+\r
+ ``default`` is the default value for this column. If the data source\r
+ does not provide None for a row, the default will be used instead. Note\r
+ that this currently affects ordering.\r
+\r
+ You can use ``visible`` to flag the column as hidden.\r
+\r
+ Setting ``sortable`` to False will result in this column being unusable\r
+ in ordering.\r
+ """\r
+ # Tracks each time a Column instance is created. Used to retain order.\r
+ creation_counter = 0\r
+\r
+ def __init__(self, verbose_name=None, name=None, default=None,\r
+ visible=True, sortable=True):\r
+ self.verbose_name = verbose_name\r
+ self.name = name\r
+ self.default = default\r
+ self.visible = visible\r
+ self.sortable = sortable\r
+ \r
+ self.creation_counter = Column.creation_counter\r
+ Column.creation_counter += 1\r
+\r
+class TextColumn(Column):\r
+ pass\r
+\r
+class NumberColumn(Column):\r
+ pass
\ No newline at end of file
--- /dev/null
+from tables import BaseTable, DeclarativeColumnsMetaclass\r
+\r
+__all__ = ('BaseModelTable', 'ModelTable')\r
+\r
+class ModelTableOptions(object):\r
+ def __init__(self, options=None):\r
+ self.model = getattr(options, 'model', None)\r
+ self.columns = getattr(options, 'columns', None)\r
+ self.exclude = getattr(options, 'exclude', None)\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 (fields and not f.name in fields) or \\r
+ (exclude and f.name in exclude):\r
+ continue\r
+ column = Column()\r
+ if column:\r
+ field_list.append((f.name, column))\r
+ return SortedDict(field_list)\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.fields, opts.exclude)\r
+ columns.update(self.declared_columns)\r
+ self.base_columns = columns\r
+ return self\r
+\r
+class ModelDataProxy(object):\r
+ pass\r
+\r
+class BaseModelTable(BaseTable):\r
+ def __init__(self, data, *args, **kwargs):\r
+ super(BaseModelTable, self).__init__([], *args, **kwargs)\r
+ if isinstance(data, models.Model):\r
+ self.queryset = data._meta.default_manager.all()\r
+ else:\r
+ self.queryset = data\r
+\r
+ def _get_data(self):\r
+ """Overridden. Return a proxy object so we don't need to load the\r
+ complete queryset.\r
+ # TODO: we probably simply want to build the queryset\r
+ """\r
+ if self._data_cache is None:\r
+ self._data_cache = ModelDataProxy(self.queryset)\r
+ return self._data_cache\r
+\r
+class ModelTable(BaseModelTable):\r
+ __metaclass__ = ModelTableMetaclass\r
--- /dev/null
+import copy\r
+from django.utils.datastructures import SortedDict\r
+from django.utils.encoding import StrAndUnicode\r
+from columns import Column\r
+\r
+__all__ = ('BaseTable', 'Table', 'Row')\r
+\r
+def sort_table(data, order_by):\r
+ """Sort a list of dicts according to the fieldnames in the\r
+ ``order_by`` iterable. Prefix with hypen for reverse.\r
+ """\r
+ def _cmp(x, y):\r
+ for name, reverse in instructions:\r
+ res = cmp(x.get(name), y.get(name))\r
+ if res != 0:\r
+ return reverse and -res or res\r
+ return 0\r
+ instructions = []\r
+ for o in order_by:\r
+ if o.startswith('-'):\r
+ instructions.append((o[1:], True,))\r
+ else:\r
+ instructions.append((o, False,))\r
+ data.sort(cmp=_cmp)\r
+\r
+class Row(object):\r
+ def __init__(self, data):\r
+ self.data = data\r
+ def as_html(self):\r
+ pass\r
+\r
+from smartinspect.auto import *\r
+si.enabled = True\r
+\r
+class DeclarativeColumnsMetaclass(type):\r
+ """\r
+ Metaclass that converts Column attributes to a dictionary called\r
+ 'base_columns', taking into account parent class 'base_columns'\r
+ as well.\r
+ """\r
+ @si_main.track\r
+ def __new__(cls, name, bases, attrs, parent_cols_from=None):\r
+ """\r
+ The ``parent_cols_from`` argument determins from which attribute\r
+ we read the columns of a base class that this table might be\r
+ subclassing. This is useful for ``ModelTable`` (and possibly other\r
+ derivatives) which might want to differ between the declared columns\r
+ and others.\r
+\r
+ Note that if the attribute specified in ``parent_cols_from`` is not\r
+ found, we fall back to the default (``base_columns``), instead of\r
+ skipping over that base. This makes a table like the following work:\r
+\r
+ class MyNewTable(tables.ModelTable, MyNonModelTable):\r
+ pass\r
+\r
+ ``MyNewTable`` will be built by the ModelTable metaclass, which will\r
+ call this base with a modified ``parent_cols_from`` argument\r
+ specific to ModelTables. Since ``MyNonModelTable`` is not a\r
+ ModelTable, and thus does not provide that attribute, the columns\r
+ from that base class would otherwise be ignored.\r
+ """\r
+\r
+ # extract declared columns\r
+ columns = [(column_name, attrs.pop(column_name))\r
+ for column_name, obj in attrs.items()\r
+ if isinstance(obj, Column)]\r
+ columns.sort(lambda x, y: cmp(x[1].creation_counter,\r
+ y[1].creation_counter))\r
+\r
+ # If this class is subclassing other tables, add their fields as\r
+ # well. Note that we loop over the bases in *reverse* - this is\r
+ # necessary to preserve the correct order of columns.\r
+ for base in bases[::-1]:\r
+ col_attr = (parent_cols_from and hasattr(base, parent_cols_from)) \\r
+ and parent_cols_from\\r
+ or 'base_columns'\r
+ if hasattr(base, col_attr):\r
+ columns = getattr(base, col_attr).items() + columns\r
+ # Note that we are reusing an existing ``base_columns`` attribute.\r
+ # This is because in certain inheritance cases (mixing normal and\r
+ # ModelTables) this metaclass might be executed twice, and we need\r
+ # to avoid overriding previous data (because we pop() from attrs,\r
+ # the second time around columns might not be registered again).\r
+ # An example would be:\r
+ # class MyNewTable(MyOldNonModelTable, tables.ModelTable): pass\r
+ if not 'base_columns' in attrs:\r
+ attrs['base_columns'] = SortedDict()\r
+ attrs['base_columns'].update(SortedDict(columns))\r
+\r
+ return type.__new__(cls, name, bases, attrs)\r
+\r
+class BaseTable(object):\r
+ def __init__(self, data, order_by=None):\r
+ """Create a new table instance with the iterable ``data``.\r
+\r
+ If ``order_by`` is specified, the data will be sorted accordingly.\r
+\r
+ Note that unlike a ``Form``, tables are always bound to data.\r
+ """\r
+ self._data = data\r
+ self._data_cache = None # will store output dataset (ordered...)\r
+ self._row_cache = None # will store Row objects\r
+ self._order_by = order_by\r
+\r
+ # The base_columns class attribute is the *class-wide* definition\r
+ # of columns. Because a particular *instance* of the class might\r
+ # want to alter self.columns, we create self.columns here by copying\r
+ # ``base_columns``. Instances should always modify self.columns;\r
+ # they should not modify self.base_columns.\r
+ self.columns = copy.deepcopy(self.base_columns)\r
+\r
+ def _build_data_cache(self):\r
+ snapshot = copy.copy(self._data)\r
+ for row in snapshot:\r
+ # delete unknown column, and add missing ones\r
+ for column in row.keys():\r
+ if not column in self.columns:\r
+ del row[column]\r
+ for column in self.columns.keys():\r
+ if not column in row:\r
+ row[column] = self.columns[column].default\r
+ if self.order_by:\r
+ sort_table(snapshot, self.order_by)\r
+ self._data_cache = snapshot\r
+\r
+ def _get_data(self):\r
+ if self._data_cache is None:\r
+ self._build_data_cache()\r
+ return self._data_cache\r
+ data = property(lambda s: s._get_data())\r
+\r
+ def _set_order_by(self, value):\r
+ if self._data_cache is not None:\r
+ self._data_cache = None\r
+ self._order_by = isinstance(value, (tuple, list)) and value or (value,)\r
+ # validate, remove all invalid order instructions\r
+ def can_be_used(o):\r
+ c = (o[:1]=='-' and [o[1:]] or [o])[0]\r
+ return c in self.columns and self.columns[c].sortable\r
+ self._order_by = [o for o in self._order_by if can_be_used(o)]\r
+ # TODO: optionally, throw an exception\r
+ order_by = property(lambda s: s._order_by, _set_order_by)\r
+\r
+ def __unicode__(self):\r
+ return self.as_html()\r
+\r
+ def __iter__(self):\r
+ for name, column in self.columns.items():\r
+ yield BoundColumn(self, column, name)\r
+\r
+ def __getitem__(self, name):\r
+ try:\r
+ column = self.columns[name]\r
+ except KeyError:\r
+ raise KeyError('Key %r not found in Table' % name)\r
+ return BoundColumn(self, column, name)\r
+\r
+ def as_html(self):\r
+ pass\r
+\r
+class Table(BaseTable):\r
+ "A collection of columns, plus their associated data rows."\r
+ # This is a separate class from BaseTable in order to abstract the way\r
+ # self.columns is specified.\r
+ __metaclass__ = DeclarativeColumnsMetaclass\r
+\r
+class BoundColumn(StrAndUnicode):\r
+ """'Runtime' version of ``Column`` that is bound to a table instance,\r
+ and thus knows about the table's data.\r
+ """\r
+ def _get_values(self):\r
+ # build a list of values used\r
+ pass\r
+ values = property(_get_values)\r
+\r
+ def __unicode__(self):\r
+ """Renders this field as an HTML widget."""\r
+ return self.as_html()\r
+\r
+ def as_html(self):\r
+ pass
\ No newline at end of file
--- /dev/null
+import os, sys\r
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))\r
+\r
+import django_tables as tables\r
+\r
+def test_declaration():\r
+ """\r
+ Test defining tables by declaration.\r
+ """\r
+\r
+ class GeoAreaTable(tables.Table):\r
+ name = tables.Column()\r
+ population = tables.Column()\r
+\r
+ assert len(GeoAreaTable.base_columns) == 2\r
+ assert 'name' in GeoAreaTable.base_columns\r
+ assert not hasattr(GeoAreaTable, 'name')\r
+\r
+ class CountryTable(GeoAreaTable):\r
+ capital = tables.Column()\r
+\r
+ assert len(CountryTable.base_columns) == 3\r
+ assert 'capital' in CountryTable.base_columns\r
+\r
+ # multiple inheritance\r
+ class AddedMixin(tables.Table):\r
+ added = tables.Column()\r
+ class CityTable(GeoAreaTable, AddedMixin):\r
+ mayer = tables.Column()\r
+\r
+ assert len(CityTable.base_columns) == 4\r
+ assert 'added' in CityTable.base_columns\r
+\r
+ # modelforms: support switching from a non-model table hierarchy to a\r
+ # modeltable hierarchy (both base class orders)\r
+ class StateTable1(tables.ModelTable, GeoAreaTable):\r
+ motto = tables.Column()\r
+ class StateTable2(GeoAreaTable, tables.ModelTable):\r
+ motto = tables.Column()\r
+\r
+ assert len(StateTable1.base_columns) == len(StateTable2.base_columns) == 3\r
+ assert 'motto' in StateTable1.base_columns\r
+ assert 'motto' in StateTable2.base_columns\r
+\r
+ # test more with actual models\r
+\r
+def test_basic():\r
+ class BookTable(tables.Table):\r
+ name = tables.Column()\r
+ books = BookTable([\r
+ {'id': 1, 'name': 'Foo: Bar'},\r
+ ])\r
+ # access without order_by works\r
+ books.data\r
+ # unknown fields are removed\r
+ for d in books.data:\r
+ assert not 'id' in d\r
+\r
+def test_sort():\r
+ class BookTable(tables.Table):\r
+ id = tables.Column()\r
+ name = tables.Column()\r
+ pages = tables.Column()\r
+ language = tables.Column(default='en') # default affects sorting\r
+\r
+ books = BookTable([\r
+ {'id': 1, 'pages': 60, 'name': 'Z: The Book'}, # language: en\r
+ {'id': 2, 'pages': 100, 'language': 'de', 'name': 'A: The Book'},\r
+ {'id': 3, 'pages': 80, 'language': 'de', 'name': 'A: The Book, Vol. 2'},\r
+ {'id': 4, 'pages': 110, 'language': 'fr', 'name': 'A: The Book, French Edition'},\r
+ ])\r
+\r
+ def test_order(order, result):\r
+ books.order_by = order\r
+ assert [b['id'] for b in books.data] == result\r
+\r
+ # test various orderings\r
+ test_order(('pages',), [1,3,2,4])\r
+ test_order(('-pages',), [4,2,3,1])\r
+ test_order('-pages', [4,2,3,1]) # using a simple string\r
+ test_order(('name',), [2,4,3,1])\r
+ test_order(('language', 'pages'), [3,2,1,4])\r
+\r
+ # test invalid order instructions\r
+ books.order_by = 'xyz'\r
+ assert not books.order_by\r
+ books.columns['language'].sortable = False\r
+ books.order_by = 'language'\r
+ assert not books.order_by\r
+ test_order(('language', 'pages'), [1,3,2,4]) # as if: 'pages'\r
+\r
+\r
+test_declaration()\r
+test_basic()\r
+test_sort()
\ No newline at end of file