1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 """Various utility functions and classes that support common presentation
16 tasks such as grouping or pagination.
17 """
18
19 from math import ceil
20 import re
21
22 __all__ = ['classes', 'first_last', 'group', 'istext', 'prepared_paginate',
23 'paginate', 'Paginator']
24
25
27 """Helper function for dynamically assembling a list of CSS class names
28 in templates.
29
30 Any positional arguments are added to the list of class names. All
31 positional arguments must be strings:
32
33 >>> classes('foo', 'bar')
34 u'foo bar'
35
36 In addition, the names of any supplied keyword arguments are added if they
37 have a truth value:
38
39 >>> classes('foo', bar=True)
40 u'foo bar'
41 >>> classes('foo', bar=False)
42 u'foo'
43
44 If none of the arguments are added to the list, this function returns
45 `None`:
46
47 >>> classes(bar=False)
48 """
49 classes = list(filter(None, args)) + [k for k, v in kwargs.items() if v]
50 if not classes:
51 return None
52 return u' '.join(classes)
53
55 return classes(first=idx == 0, last=idx == len(seq) - 1)
56
57
58 -def group(iterable, num, predicate=None):
59 """Combines the elements produced by the given iterable so that every `n`
60 items are returned as a tuple.
61
62 >>> items = [1, 2, 3, 4]
63 >>> for item in group(items, 2):
64 ... print item
65 (1, 2)
66 (3, 4)
67
68 The last tuple is padded with `None` values if its' length is smaller than
69 `num`.
70
71 >>> items = [1, 2, 3, 4, 5]
72 >>> for item in group(items, 2):
73 ... print item
74 (1, 2)
75 (3, 4)
76 (5, None)
77
78 The optional `predicate` parameter can be used to flag elements that should
79 not be packed together with other items. Only those elements where the
80 predicate function returns True are grouped with other elements, otherwise
81 they are returned as a tuple of length 1:
82
83 >>> items = [1, 2, 3, 4]
84 >>> for item in group(items, 2, lambda x: x != 3):
85 ... print item
86 (1, 2)
87 (3,)
88 (4, None)
89 """
90 buf = []
91 for item in iterable:
92 flush = predicate and not predicate(item)
93 if buf and flush:
94 buf += [None] * (num - len(buf))
95 yield tuple(buf)
96 del buf[:]
97 buf.append(item)
98 if flush or len(buf) == num:
99 yield tuple(buf)
100 del buf[:]
101 if buf:
102 buf += [None] * (num - len(buf))
103 yield tuple(buf)
104
105
107 from genshi.core import Markup
108 return isinstance(text, basestring) and not isinstance(text, Markup)
109
111 if max_per_page == 0:
112 num_pages = 1
113 else:
114 num_pages = int(ceil(float(num_items) / max_per_page))
115 return items, num_items, num_pages
116
117 -def paginate(items, page=0, max_per_page=10):
118 """Simple generic pagination.
119
120 Given an iterable, this function returns:
121 * the slice of objects on the requested page,
122 * the total number of items, and
123 * the total number of pages.
124
125 The `items` parameter can be a list, tuple, or iterator:
126
127 >>> items = range(12)
128 >>> items
129 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
130 >>> paginate(items)
131 ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 12, 2)
132 >>> paginate(items, page=1)
133 ([10, 11], 12, 2)
134 >>> paginate(iter(items))
135 ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 12, 2)
136 >>> paginate(iter(items), page=1)
137 ([10, 11], 12, 2)
138
139 This function also works with generators:
140
141 >>> def generate():
142 ... for idx in range(12):
143 ... yield idx
144 >>> paginate(generate())
145 ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 12, 2)
146 >>> paginate(generate(), page=1)
147 ([10, 11], 12, 2)
148
149 The `max_per_page` parameter can be used to set the number of items that
150 should be displayed per page:
151
152 >>> items = range(12)
153 >>> paginate(items, page=0, max_per_page=6)
154 ([0, 1, 2, 3, 4, 5], 12, 2)
155 >>> paginate(items, page=1, max_per_page=6)
156 ([6, 7, 8, 9, 10, 11], 12, 2)
157 """
158 if not page:
159 page = 0
160 start = page * max_per_page
161 stop = start + max_per_page
162
163 count = None
164 if hasattr(items, '__len__'):
165 count = len(items)
166 if count:
167 assert start < count, 'Page %d out of range' % page
168
169 try:
170 retval = items[start:stop]
171 except TypeError:
172 retval = []
173 idx = -1
174 for idx, item in enumerate(items):
175 if start <= idx < stop:
176 retval.append(item)
177
178
179
180 if count is not None and idx >= stop:
181 break
182 if count is None:
183 count = idx + 1
184
185 return retval, count, int(ceil(float(count) / max_per_page))
186
187
189
190 - def __init__(self, items, page=0, max_per_page=10, num_items=None):
191 if not page:
192 page = 0
193
194 if num_items is None:
195 items, num_items, num_pages = paginate(items, page, max_per_page)
196 else:
197 items, num_items, num_pages = prepared_paginate(items, num_items,
198 max_per_page)
199 offset = page * max_per_page
200 self.page = page
201 self.max_per_page = max_per_page
202 self.items = items
203 self.num_items = num_items
204 self.num_pages = num_pages
205 self.span = offset, offset + len(items)
206 self.show_index = True
207
209 return iter(self.items)
210
212 return len(self.items)
213
215 return len(self.items) > 0
216
218 self.items[idx] = value
219
220 - def has_more_pages(self):
221 return self.num_pages > 1
222 has_more_pages = property(has_more_pages)
223
224 - def has_next_page(self):
225 return self.page + 1 < self.num_pages
226 has_next_page = property(has_next_page)
227
230 has_previous_page = property(has_previous_page)
231
232 - def get_shown_pages(self, page_index_count = 11):
233 if self.has_more_pages == False:
234 return range(1, 2)
235
236 min_page = 1
237 max_page = int(ceil(float(self.num_items) / self.max_per_page))
238 current_page = self.page + 1
239 start_page = current_page - page_index_count / 2
240 end_page = current_page + page_index_count / 2 + \
241 (page_index_count % 2 - 1)
242
243 if start_page < min_page:
244 start_page = min_page
245 if end_page > max_page:
246 end_page = max_page
247
248 return range(start_page, end_page + 1)
249
251 from trac.util.translation import _
252 start, stop = self.span
253 total = self.num_items
254 if start+1 == stop:
255 return _("%(last)d of %(total)d", last=stop, total=total)
256 else:
257 return _("%(start)d - %(stop)d of %(total)d",
258 start=self.span[0]+1, stop=self.span[1], total=total)
259
260
262 """Yield `(item, sep)` tuples, one for each element in `items`.
263
264 `sep` will be `None` for the last item.
265
266 >>> list(separated([1, 2]))
267 [(1, ','), (2, None)]
268
269 >>> list(separated([1]))
270 [(1, None)]
271
272 >>> list(separated("abc", ':'))
273 [('a', ':'), ('b', ':'), ('c', None)]
274 """
275 items = iter(items)
276 last = items.next()
277 for i in items:
278 yield last, sep
279 last = i
280 yield last, None
281
282
283 try:
284 from json import dumps
285
286 _js_quote = dict((c, '\\u%04x' % ord(c)) for c in '&<>')
287 _js_quote_re = re.compile('[' + ''.join(_js_quote) + ']')
288
290 """Encode `value` to JSON."""
291 def replace(match):
292 return _js_quote[match.group(0)]
293 text = dumps(value, sort_keys=True, separators=(',', ':'))
294 return _js_quote_re.sub(replace, text)
295
296 except ImportError:
297 from trac.util.text import javascript_quote
298
300 """Encode `value` to JSON."""
301 if isinstance(value, basestring):
302 return '"%s"' % javascript_quote(value)
303 elif value is None:
304 return 'null'
305 elif value is False:
306 return 'false'
307 elif value is True:
308 return 'true'
309 elif isinstance(value, (int, long)):
310 return str(value)
311 elif isinstance(value, float):
312 return repr(value)
313 elif isinstance(value, (list, tuple)):
314 return '[%s]' % ','.join(to_json(each) for each in value)
315 elif isinstance(value, dict):
316 return '{%s}' % ','.join('%s:%s' % (to_json(k), to_json(v))
317 for k, v in sorted(value.iteritems()))
318 else:
319 raise TypeError('Cannot encode type %s' % value.__class__.__name__)
320