Версия 16 от 2010-07-13 12:37:43

Убрать это сообщение

z3c.table - продвинутые таблицы

Z3C Table

Цель, которую преследует пакет z3c.table - предложить модульную библиотеку для отрисовки таблиц. Мы используем шаблон "контент провайдер" с колонками, реализованными как адаптеры. Такой подход - мощная базовая концепция.

Важные требования

Никаких скинов

Этот пакет не предоставляет никаких шаблонов и скинов. В любом случае, когда вам нужно отрисовать красивую таблицу, вам придется писать свой собственный скин или шаблон. Отсутствие шаблонов и скинов позволяет удостоверится, что z3c.table имеет очень мало зависимостей, а поэтому легко поддается повторному использованию.

Заметка

Как вы, вероятно, уже знаете, перед тем, как выполняется сортировка по столбцам при порционном отображении данных таблицы, должна быть выполнена сортировка полного набора данных. При большом наборе данных это ведет к проблемам с быстродействием. При работе с большими объемами данных мы рекомендуем не совмещать порционное отображение с сортировкой по столбцам, либо, если нужно, предоставлять "умный" механизм кеширования данных для хранения отсортированных последовательностей.

Пример установки данных

Таблицы часто используются для отображения нормализованных порций данных. Например, нам необходимо отобразить информацию о файлах в папке. Каждый файл имеет заголовок, размер и тип. Наша таблица должна иметь строку для каждого файла и столбец для этих трех полей данных (заголовка, размера, и типа). Контекстом таблицы всегда является структура данных, по которой можно выполнить итерацию, при этом каждый элемент последовательности соответствует строке таблицы. Давайте создадим папку, которую мы сможем использовать как контекст:

   1 >>> from zope.app.container import btree
   2 >>> class Folder(btree.BTreeContainer):
   3 ...     """Sample folder."""
   4 ...     __name__ = u'folder'
   5 >>> folder = Folder()

XXX: не уверен, куда нам нужно положить эту папку. Также нам можно и не давать значение атрибуту _ _name_ _. Нам не нужно куда либо помещать ее. Давайте установим родительский элемент для папки:

   1 >>> root['folder'] = folder

Теперь создадим простой объект File для наполнения созданной нами папки.

   1 >>> class File(object):
   2 ...     """Sample file."""
   3 ...     def __init__(self, title, size, type=None):
   4 ...         self.title = title
   5 ...         self.number = size
   6 ...         self.type = type

Теперь давайте наполним созданную папку файлами.

   1 >>> folder[u'first'] = File('First', 1)
   2 >>> folder[u'second'] = File('Second', 2)
   3 >>> folder[u'third'] = File('Third', 3)

Создание таблиц

Теперь, когда у нас есть тестовые данные, с которыми можно работать, мы может создать таблицу. Так как таблицы - компоненты пользовательского интерфейса, они требуют и контекста и запроса (request). Они передаются как аргументы конструктору класса Table.

   1 >>> from zope.publisher.browser import TestRequest
   2 >>> from z3c.table import table
   3 >>> request = TestRequest()
   4 >>> plainTable = table.Table(folder, request)

Когда таблица создана, мы можем ее обновлять и отрисовывать. Так как мы не указали столбцы таблицы, которые нужно отрисовать, таблица отрисуется как пустая строка:

   1 >>> plainTable.update()
   2 >>> plainTable.render()
   3 u''

Также стоит заметить, что класс Table - реализация интерфейса ITable. Намного интереснее взглянуть, что предоставляет ITable, когда у нас есть несколько столбцов:

   1 >>> from z3c.table import interfaces
   2 >>> from zope.interface.verify import verifyObject
   3 >>> verifyObject(interfaces.ITable, plainTable)
   4 True

Создание столбцов

Так как нам может понадобится некий тип столбцов в многих разных таблицах, определение столбца лежит отдельно от самой таблицы. Каждый тип столбцов представлен собственным классом, который реализует интерфейс IColumn. Чтобы помочь в определении столбца, существует базовый класс Column. Простая колонка, которая отображает заголовок элемента выглядит примерно так:

   1 >>> from z3c.table import column
   2 >>> class TitleColumn(column.Column):
   3 ...
   4 ...     weight = 10
   5 ...     header = u'Title'
   6 ...
   7 ...     def renderCell(self, item):
   8 ...         return u'Title: %s' % item.title

Атрибут header - это текст, который мы можем увидеть в заголовке таблицы, обычно это тег <th>. Атрибут weight указывает порядок колонки в таблице относительно других колонок. Метод renderCell делает всю работу по отображению, возвращая html структуру, которая будет находится внутри колонки таблица, обычно это тег <td>. Метод renderCell должен предоставляться потомками класса Column.

Добавление колонки в таблицу с использованием адаптеров

Одним из способов добавления колонки в таблицу является адаптер. Хотя сначала такой подход кажется избыточным, он делает таблицы легко подключаемыми. Давайте зарегистрируем колонку как адаптер.

   1 >>> import zope.component
   2 >>> zope.component.provideAdapter(TitleColumn,
   3 ...     (None, None, interfaces.ITable), provides=interfaces.IColumn,
   4 ...      name='firstColumn')

Теперь отрисуем таблицу еще раз:

   1 >>> plainTable.update()
   2 >>> print plainTable.render()

   1 <table>
   2   <thead>
   3     <tr>
   4       <th>Title</th>
   5     </tr>
   6   </thead>
   7   <tbody>
   8     <tr>
   9       <td>Title: First</td>
  10     </tr>
  11     <tr>
  12       <td>Title: Second</td>
  13     </tr>
  14     <tr>
  15       <td>Title: Third</td>
  16     </tr>
  17   </tbody>
  18 </table>

Мы также можем использовать предопределенное имя столбца:

   1 >>> zope.component.provideAdapter(column.NameColumn,
   2 ...     (None, None, interfaces.ITable), provides=interfaces.IColumn,
   3 ...      name='secondColumn')

Теперь мы получим еще один дополнительный столбец:

   1 >>> plainTable.update()
   2 >>> print plainTable.render()

   1 <table>
   2   <thead>
   3     <tr>
   4       <th>Name</th>
   5       <th>Title</th>
   6     </tr>
   7   </thead>
   8   <tbody>
   9     <tr>
  10       <td>first</td>
  11       <td>Title: First</td>
  12     </tr>
  13     <tr>
  14       <td>second</td>
  15       <td>Title: Second</td>
  16     </tr>
  17     <tr>
  18       <td>third</td>
  19       <td>Title: Third</td>
  20     </tr>
  21   </tbody>
  22 </table>

Объединение ячеек

Теперь давайте посмотрим, как можно сделать объединение ячеек для столбца:

   1 >>> class ColspanColumn(column.NameColumn):
   2 ...
   3 ...     weight = 999
   4 ...
   5 ...     def getColspan(self, item):
   6 ...         # colspan condition
   7 ...         if item.__name__ == 'first':
   8 ...             return 2
   9 ...         else:
  10 ...             return 0
  11 ...
  12 ...     def renderHeadCell(self):
  13 ...         return u'Colspan'
  14 ...
  15 ...     def renderCell(self, item):
  16 ...         return u'colspan: %s' % item.title

Теперь зарегистрируем адаптер этого столбца как colspanColumn:

   1 >>> zope.component.provideAdapter(ColspanColumn,
   2 ...     (None, None, interfaces.ITable), provides=interfaces.IColumn,
   3 ...      name='colspanColumn')

Теперь вы видите, насколько объединение через ColspanAdapter больше, чем столбцы таблицы. Такой код вызовет исключение ValueError:

   1 >>> plainTable.update()
   2 ...
   3 ValueError: Colspan for column '<ColspanColumn u'colspanColumn'>' larger then table.

Но если мы установим столбец первой строчкой, таблица отрисуется корректно:

   1 >>> class CorrectColspanColumn(ColspanColumn):
   2 ...     """Colspan with correct weight."""
   3 ...
   4 ...     weight = 0

Зарегистрируйте и отрисуйте таблицу еще раз:

   1 >>> zope.component.provideAdapter(CorrectColspanColumn,
   2 ...     (None, None, interfaces.ITable), provides=interfaces.IColumn,
   3 ...      name='colspanColumn')
   4 >>> plainTable.update()
   5 >>> print plainTable.render()

   1 <table>
   2   <thead>
   3     <tr>
   4       <th>Colspan</th>
   5       <th>Name</th>
   6       <th>Title</th>
   7     </tr>
   8   </thead>
   9   <tbody>
  10     <tr>
  11       <td colspan="2">colspan: First</td>
  12       <td>Title: First</td>
  13     </tr>
  14     <tr>
  15       <td>colspan: Second</td>
  16       <td>second</td>
  17       <td>Title: Second</td>
  18     </tr>
  19     <tr>
  20       <td>colspan: Third</td>
  21       <td>third</td>
  22       <td>Title: Third</td>
  23     </tr>
  24   </tbody>
  25 </table>

Установка колонок

Существующая реализация позволяет определять таблицу в классе без использования адаптеров.

Сначала нужно определить столбец, который сможет отобразить значения наших элементов:

   1 >>> class SimpleColumn(column.Column):
   2 ...
   3 ...     weight = 0
   4 ...
   5 ...     def renderCell(self, item):
   6 ...         return item.title

Давайте определим нашу таблицу, которая явно определяет столбцы. Вы также можете увидеть, что мы не возвращаем колонки в правильном порядке:

   1 >>> class PrivateTable(table.Table):
   2 ...
   3 ...     def setUpColumns(self):
   4 ...         firstColumn = TitleColumn(self.context, self.request, self)
   5 ...         firstColumn.__name__ = u'title'
   6 ...         firstColumn.weight = 1
   7 ...         secondColumn = SimpleColumn(self.context, self.request, self)
   8 ...         secondColumn.__name__ = u'simple'
   9 ...         secondColumn.weight = 2
  10 ...         secondColumn.header = u'The second column'
  11 ...         return [secondColumn, firstColumn]

Теперь мы можете создавать, обновлять и отображать таблицу:

   1 >>> privateTable = PrivateTable(folder, request)
   2 >>> privateTable.update()
   3 >>> print privateTable.render()

   1 <table>
   2   <thead>
   3     <tr>
   4       <th>Title</th>
   5       <th>The second column</th>
   6     </tr>
   7   </thead>
   8   <tbody>
   9     <tr>
  10       <td>Title: First</td>
  11       <td>First</td>
  12     </tr>
  13     <tr>
  14       <td>Title: Second</td>
  15       <td>Second</td>
  16     </tr>
  17     <tr>
  18       <td>Title: Third</td>
  19       <td>Third</td>
  20     </tr>
  21   </tbody>
  22 </table>

Каскадные таблицы стилей

Наша реализация таблицы и столбца поддерживает установку классов css. Давайте определим таблицу и столбцы с некими значениями css:

   1 >>> class CSSTable(table.Table):
   2 ...
   3 ...     cssClasses = {'table': 'table',
   4 ...                   'thead': 'thead',
   5 ...                   'tbody': 'tbody',
   6 ...                   'th': 'th',
   7 ...                   'tr': 'tr',
   8 ...                   'td': 'td'}
   9 ...
  10 ...     def setUpColumns(self):
  11 ...         firstColumn = TitleColumn(self.context, self.request, self)
  12 ...         firstColumn.__name__ = u'title'
  13 ...         firstColumn.__parent__ = self
  14 ...         firstColumn.weight = 1
  15 ...         firstColumn.cssClasses = {'th':'thCol', 'td':'tdCol'}
  16 ...         secondColumn = SimpleColumn(self.context, self.request, self)
  17 ...         secondColumn.__name__ = u'simple'
  18 ...         secondColumn.__parent__ = self
  19 ...         secondColumn.weight = 2
  20 ...         secondColumn.header = u'The second column'
  21 ...         return [secondColumn, firstColumn]

Теперь посмотрит, как отображается такая таблица с учетом присвоенных значений css классов. Заметьте, что th и td получили из таблицы и из колонки.

   1 >>> cssTable = CSSTable(folder, request)
   2 >>> cssTable.update()
   3 >>> print cssTable.render()

   1 <table class="table">
   2   <thead class="thead">
   3     <tr class="tr">
   4       <th class="thCol th">Title</th>
   5       <th class="th">The second column</th>
   6     </tr>
   7   </thead>
   8   <tbody class="tbody">
   9     <tr class="tr">
  10       <td class="tdCol td">Title: First</td>
  11       <td class="td">First</td>
  12     </tr>
  13     <tr class="tr">
  14       <td class="tdCol td">Title: Second</td>
  15       <td class="td">Second</td>
  16     </tr>
  17     <tr class="tr">
  18       <td class="tdCol td">Title: Third</td>
  19       <td class="td">Third</td>
  20     </tr>
  21   </tbody>
  22 </table>

Перемежающиеся таблицы

Мы поддерживаем встроенную поддержку перемежающихся строк таблицы, которые основаны на парных и непарных классах CSS. Давайте определим таблицу, включая другие CSS классы. Для поддержки парности/непарности нам следует определить классу cssClassEven и cssClassOdd CSS:

   1 >>> class AlternatingTable(table.Table):
   2 ...
   3 ...     cssClasses = {'table': 'table',
   4 ...                   'thead': 'thead',
   5 ...                   'tbody': 'tbody',
   6 ...                   'th': 'th',
   7 ...                   'tr': 'tr',
   8 ...                   'td': 'td'}
   9 ...
  10 ...     cssClassEven = u'even'
  11 ...     cssClassOdd = u'odd'
  12 ...
  13 ...     def setUpColumns(self):
  14 ...         firstColumn = TitleColumn(self.context, self.request, self)
  15 ...         firstColumn.__name__ = u'title'
  16 ...         firstColumn.__parent__ = self
  17 ...         firstColumn.weight = 1
  18 ...         firstColumn.cssClasses = {'th':'thCol', 'td':'tdCol'}
  19 ...         secondColumn = SimpleColumn(self.context, self.request, self)
  20 ...         secondColumn.__name__ = u'simple'
  21 ...         secondColumn.__parent__ = self
  22 ...         secondColumn.weight = 2
  23 ...         secondColumn.header = u'The second column'
  24 ...         return [secondColumn, firstColumn]

Теперь обновите и отобразите новую таблицу. Как видите, к данному tr классу добавились дополнительные классы even и odd:

   1 >>> alternatingTable = AlternatingTable(folder, request)
   2 >>> alternatingTable.update()
   3 >>> print alternatingTable.render()

   1 <table class="table">
   2   <thead class="thead">
   3     <tr class="tr">
   4       <th class="thCol th">Title</th>
   5       <th class="th">The second column</th>
   6     </tr>
   7   </thead>
   8   <tbody class="tbody">
   9     <tr class="even tr">
  10       <td class="tdCol td">Title: First</td>
  11       <td class="td">First</td>
  12     </tr>
  13     <tr class="odd tr">
  14       <td class="tdCol td">Title: Second</td>
  15       <td class="td">Second</td>
  16     </tr>
  17     <tr class="even tr">
  18       <td class="tdCol td">Title: Third</td>
  19       <td class="td">Third</td>
  20     </tr>
  21   </tbody>
  22 </table>

Сортировка таблицы

Еще одно свойство таблицы - поддержка сортировки данных по столбцам. Так как сортировка данных таблицы вещь очень важная, мы предлагаем ее по умолчанию. Но ее можно использовать только тогда, когда установлено значение sortOn. Вы можете установить это значение на уровне класса, добавляя значение defaultSortOn или устанавливая его как значение в запросе. Мы покажем вам, как это сделать позже. Нам также потребуются столбцы, которые помогут продемонстрировать хороший пример сортировки. Наш новый столбец, по которому будем сортировать, будет использовать атрибут number элементов содержимого в качестве критерия сортировки:

   1 >>> class NumberColumn(column.Column):
   2 ...
   3 ...     header = u'Number'
   4 ...     weight = 20
   5 ...
   6 ...     def getSortKey(self, item):
   7 ...         return item.number
   8 ...
   9 ...     def renderCell(self, item):
  10 ...         return 'number: %s' % item.number

Теперь давайте установим таблицу:

   1 >>> class SortingTable(table.Table):
   2 ...
   3 ...     def setUpColumns(self):
   4 ...         firstColumn = TitleColumn(self.context, self.request, self)
   5 ...         firstColumn.__name__ = u'title'
   6 ...         firstColumn.__parent__ = self
   7 ...         secondColumn = NumberColumn(self.context, self.request, self)
   8 ...         secondColumn.__name__ = u'number'
   9 ...         secondColumn.__parent__ = self
  10 ...         return [firstColumn, secondColumn]

Нам также потребуется больше элементов в контейнере, которые мы будем сортировать:

   1 >>> folder[u'fourth'] = File('Fourth', 4)
   2 >>> folder[u'zero'] = File('Zero', 0)

Давайте отобразим из без установленного значения sortOn:

   1 >>> sortingTable = SortingTable(folder, request)
   2 >>> sortingTable.update()
   3 >>> print sortingTable.render()

   1 <table>
   2   <thead>
   3     <tr>
   4       <th>Title</th>
   5       <th>Number</th>
   6     </tr>
   7   </thead>
   8   <tbody>
   9     <tr>
  10       <td>Title: First</td>
  11       <td>number: 1</td>
  12     </tr>
  13     <tr>
  14       <td>Title: Fourth</td>
  15       <td>number: 4</td>
  16     </tr>
  17     <tr>
  18       <td>Title: Second</td>
  19       <td>number: 2</td>
  20     </tr>
  21     <tr>
  22       <td>Title: Third</td>
  23       <td>number: 3</td>
  24     </tr>
  25     <tr>
  26       <td>Title: Zero</td>
  27       <td>number: 0</td>
  28     </tr>
  29   </tbody>
  30 </table>

Как видите, эта таблица не предоставляет никакого явного порядка. Давайте определим индекс столбца, по которому будем сортировать данные:

   1 >>> sortOnId = sortingTable.rows[0][1][1].id
   2 >>> sortOnId
   3 u'table-number-1'

Теперь давайте используем найденный индекс как значение sortOn:

   1 >>> sortingTable.sortOn = sortOnId

Важным моментом является обновление таблицы после установки сортировки по значению:

   1 >>> sortingTable.update()
   2 >>> print sortingTable.render()

   1 <table>
   2   <thead>
   3     <tr>
   4       <th>Title</th>
   5       <th>Number</th>
   6     </tr>
   7   </thead>
   8   <tbody>
   9     <tr>
  10       <td>Title: Zero</td>
  11       <td>number: 0</td>
  12     </tr>
  13     <tr>
  14       <td>Title: First</td>
  15       <td>number: 1</td>
  16     </tr>
  17     <tr>
  18       <td>Title: Second</td>
  19       <td>number: 2</td>
  20     </tr>
  21     <tr>
  22       <td>Title: Third</td>
  23       <td>number: 3</td>
  24     </tr>
  25     <tr>
  26       <td>Title: Fourth</td>
  27       <td>number: 4</td>
  28     </tr>
  29   </tbody>
  30 </table>

Мы также можем инвертировать порядок сортировки:

   1 >>> sortingTable.sortOrder = 'reverse'
   2 >>> sortingTable.update()
   3 >>> print sortingTable.render()

   1 <table>
   2   <thead>
   3     <tr>
   4       <th>Title</th>
   5       <th>Number</th>
   6     </tr>
   7   </thead>
   8   <tbody>
   9     <tr>
  10       <td>Title: Fourth</td>
  11       <td>number: 4</td>
  12     </tr>
  13     <tr>
  14       <td>Title: Third</td>
  15       <td>number: 3</td>
  16     </tr>
  17     <tr>
  18       <td>Title: Second</td>
  19       <td>number: 2</td>
  20     </tr>
  21     <tr>
  22       <td>Title: First</td>
  23       <td>number: 1</td>
  24     </tr>
  25     <tr>
  26       <td>Title: Zero</td>
  27       <td>number: 0</td>
  28     </tr>
  29   </tbody>
  30 </table>

Реализация таблицы также позволяет установить критерий сортировки, который передается через запрос. Давайте настроим такой запрос:

   1 >>> sorterRequest = TestRequest(form={'table-sortOn': 'table-number-1',
   2 ...                                   'table-sortOrder':'descending'})

и еще раз обновим и отобразим. Как видите, новая таблица отсортирована по втором столбце и упорядоченна в инверсном порядке:

{{{#highlight python >>> requestSortedTable = SortingTable(folder, sorterRequest) >>> requestSortedTable.update() >>> print requestSortedTable.render() }}}

   1 <table>
   2   <thead>
   3     <tr>
   4       <th>Title</th>
   5       <th>Number</th>
   6     </tr>
   7   </thead>
   8   <tbody>
   9     <tr>
  10       <td>Title: Fourth</td>
  11       <td>number: 4</td>
  12     </tr>
  13     <tr>
  14       <td>Title: Third</td>
  15       <td>number: 3</td>
  16     </tr>
  17     <tr>
  18       <td>Title: Second</td>
  19       <td>number: 2</td>
  20     </tr>
  21     <tr>
  22       <td>Title: First</td>
  23       <td>number: 1</td>
  24     </tr>
  25     <tr>
  26       <td>Title: Zero</td>
  27       <td>number: 0</td>
  28     </tr>
  29   </tbody>
  30 </table>

Перевод: Ростислав Дзинько