""" open/dulcinea/lib/ui/table.qpy """ from csv import DictWriter from dulcinea.util import StringIO from math import ceil from qp.fill.css import format_style_rule, TextStyle from qp.fill.form import Form from qp.fill.widget import OptionSelectWidget from qp.fill.html import htmltag, href from qp.lib.util import randbytes, integer from qp.pub.common import get_request, redirect, get_session from qpy import xml import sys class Table (object): def __init__(self, maximum_rows_for_client_side_sorting=10000): self.headings = [] self.rows = [] self.caption = '' self.footer = '' self.tbody_id = 'T' + randbytes(4).decode('latin1') self.sortable_headings = set() self.maximum_rows_for_client_side_sorting = \ maximum_rows_for_client_side_sorting def column(self, sortable=True, **name_content): assert len(name_content) == 1 assert self.rows == [] self.headings += list(name_content.items()) if sortable: self.sortable_headings.add(self.headings[-1][0]) def get_title(self, column): for c, title in self.headings: if column == c: return title def set_title(self, **name_content): assert len(name_content) == 1 name, title = name_content.items()[0] for j, item in enumerate(self.headings): if item[0] == name: self.headings[j] = (name, title) return def row(self, **args): self.rows.append(args) def get_rows(self): return self.rows def set_caption(self, arg): self.caption = arg def set_footer(self, arg): """ The argument should be a rendered tbody element. """ self.footer = arg def render:xml(self, **attrs): if len(self.rows) <= self.maximum_rows_for_client_side_sorting: self.render_for_client_side_sorting(**attrs) else: self.render_for_server_side_sorting(**attrs) def render_csv(self): s = StringIO() fieldnames = [key for key, value in self.headings] dict_writer = DictWriter(s, fieldnames=fieldnames) dict_writer.writerow(dict(self.headings)) for row in self.rows: dict_writer.writerow( dict([(k, v) for k, v in row.items() if k in fieldnames])) return s.getvalue() def render_rows:xml(self): keys = [key for (key, value) in self.headings] for j, row in enumerate(self.rows): classes = row.get('classes') or [] if j % 2 == 0: classes.append('even') else: classes.append('odd') attrs = dict((k, v) for k, v in row.items() if k not in keys and k != 'classes') htmltag('tr', classes=classes, **attrs) for key in keys: '' % key row.get(key) '' '' def render_tbody:xml(self): '' % self.tbody_id self.render_rows() '' def render_footer:xml(self): self.footer def render_for_server_side_sorting:xml(self, **attrs): htmltag('table', **attrs) if self.caption: '%s' % self.caption '' query_prefix = get_request().get_query() query = '' for heading in self.sortable_headings: if query_prefix.endswith(heading): query_prefix = query_prefix[:-len(heading)] query = heading heading_plus_minus = heading + '-' if query_prefix.endswith(heading_plus_minus): query_prefix = query_prefix[:-len(heading_plus_minus)] query = heading_plus_minus reversed_rows = False if query.endswith('-'): query = query[:-1] reversed_rows = True if query in self.sortable_headings: self.rows = sorted(self.rows, key=lambda row: row.get(query)) if reversed_rows: self.rows = reversed(self.rows) for name, content in self.headings: '' % name if name in self.sortable_headings: if query_prefix: new_query = '?' + query_prefix + '&' + name else: new_query = '?' + name if query.endswith(name) and not reversed_rows: new_query += '-' href(new_query, content, title="sort table using this column") if name == query: if reversed_rows: '' else: '' else: content '' '' self.render_tbody() self.render_footer() '' def render_for_client_side_sorting:xml(self, **attrs): tbody_id = repr(self.tbody_id).lstrip("u") htmltag('table', **attrs) if self.caption: '%s' % self.caption '' % self.tbody_id for index, heading in enumerate(self.headings): if heading[0] in self.sortable_headings: '' % ( heading[0], tbody_id, index) '' heading[1] '' '' '' '' else: '' % heading[0] heading[1] '' '' self.render_tbody() self.render_footer() '' if self.sortable_headings: '' % xml( self.javascript) javascript = ''' function tbody_sort(id, col) { var tbody = document.getElementById(id) function get_string_data(node) { if (node.data) { return node.data.toLowerCase() } else { var parts = [] for (var child = node.firstChild; child; child = child.nextSibling) parts = parts.concat(get_string_data(child)) return parts.join(" ").toLowerCase() } } function get_key(node) { if (node.childNodes.length == 0) { return "" } data = get_string_data(node) if (!data) { return "" } if (data.length == 0) { return "" } if (data.charAt(0) == '$') { var num = parseFloat(data.substring(1)) if (!isNaN(num)) { return num } } if (!data.match(/^\s*\d+[-\/]\d+[-\/]\d+/)) { /* data does not appear to start with a date */ var num = parseFloat(data) if (!isNaN(num)) { return num } } return data } var n = tbody.rows.length var sdata = new Array() for (var j=0;j b) { changed = 1 tmp = sdata[k] sdata[k] = sdata[k-1] sdata[k-1] = tmp } } } var headcells = document.getElementById(id+"H").firstChild.childNodes for (var j=0;j < headcells.length;j++) { child = headcells.item(j) child.className = "" } if (changed == 1) { headcells.item(col).className = "increasing" } else { headcells.item(col).className = "decreasing" sdata.reverse() } for (var j=0;j' '' % len(self.headings) '' if self.pages > 1: '' '' '' def set_pages(self, pages): self.pages = pages def set_window(self, window): self.window = window def render_thead:xml(self): '' self.render_controls() '' sort_name = self.form.get_widget('sort').get_name() for name, content in self.headings: '' % name if name not in self.sortable_headings: content else: if self.form.get('sort') == name: query = self.construct_form_query(**{sort_name:"-"+name}) href(query, content, title="reverse table row order") '' else: query = self.construct_form_query(**{sort_name:name}) href(query, content, title="sort table using this column") if self.form.get('sort') == "-" + name: '' '' '' def render:xml(self, **attrs): if len(self.rows) == 0 and self.page != 1: redirect(self.construct_form_query(page=1, start=1)) htmltag('table', **attrs) if self.caption: '%s' % self.caption self.render_thead() self.render_tbody() self.render_footer() '' class Searcher (object): """ To use with Pager, implement a subclass of this that includes the perform_search() that you need for your items. Make an instance of this with an instance of your pager class. Call get_selection() on this instance to get the items that you will use to fill in the rows of the Pager. """ def __init__(self, pager): self.sort_column_key = pager.get_sort_column_key() self.sort_reversal = pager.get_sort_reversal() self.page_size = pager.get_page_size() self.page = pager.get_page() self.query = pager.get_search_query() self.perform_search() pager.set_pages(int(ceil(self.get_number_of_matches() / float(self.page_size)))) def get_page_start(self): return self.page_size * (self.page - 1) def get_page_end(self): return self.page_size * self.page def perform_search(): """ This sets self.number_of_matches and self.selection . Subclass must implement. """ raise NotImplemented def get_number_of_matches(self): """Call perform_search() before this. """ return self.number_of_matches def get_selection(self): """Call perform_search() before this. """ return self.selection