"""
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