4eacc8cc258846563f534b4a455cc4345ff074fe
1 import sys, cgi, urllib, os, re, os.path, time, errno
3 from roundup import hyperdb, date
4 from roundup.i18n import _
6 try:
7 import cPickle as pickle
8 except ImportError:
9 import pickle
10 try:
11 import cStringIO as StringIO
12 except ImportError:
13 import StringIO
14 try:
15 import StructuredText
16 except ImportError:
17 StructuredText = None
19 # bring in the templating support
20 from roundup.cgi.PageTemplates import PageTemplate
21 from roundup.cgi.PageTemplates.Expressions import getEngine
22 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
23 from roundup.cgi import ZTUtils
25 class NoTemplate(Exception):
26 pass
28 class Templates:
29 templates = {}
31 def __init__(self, dir):
32 self.dir = dir
34 def precompileTemplates(self):
35 ''' Go through a directory and precompile all the templates therein
36 '''
37 for filename in os.listdir(self.dir):
38 if os.path.isdir(filename): continue
39 if '.' in filename:
40 name, extension = filename.split('.')
41 self.get(name, extension)
42 else:
43 self.get(filename, None)
45 def get(self, name, extension=None):
46 ''' Interface to get a template, possibly loading a compiled template.
48 "name" and "extension" indicate the template we're after, which in
49 most cases will be "name.extension". If "extension" is None, then
50 we look for a template just called "name" with no extension.
52 If the file "name.extension" doesn't exist, we look for
53 "_generic.extension" as a fallback.
54 '''
55 # default the name to "home"
56 if name is None:
57 name = 'home'
58 elif extension is None and '.' in name:
59 # split name
60 name, extension = name.split('.')
62 # find the source, figure the time it was last modified
63 if extension:
64 filename = '%s.%s'%(name, extension)
65 else:
66 filename = name
68 src = os.path.join(self.dir, filename)
69 if not os.path.exists(src):
70 filename = filename + '.html'
71 src = os.path.join(self.dir, filename)
72 if not os.path.exists(src):
73 if not extension:
74 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
76 # try for a generic template
77 generic = '_generic.%s'%extension
78 src = os.path.join(self.dir, generic)
79 if not os.path.exists(src):
80 generic = '_generic.%s.html'%extension
81 src = os.path.join(self.dir, generic)
82 if not os.path.exists(src):
83 raise NoTemplate, 'No template file exists for '\
84 'templating "%s" with template "%s" (neither '\
85 '"%s" nor "%s")'%(name, extension, filename,
86 generic)
87 filename = generic
89 try:
90 stime = os.stat(src)[os.path.stat.ST_MTIME]
91 except os.error, error:
92 if error.errno != errno.ENOENT:
93 raise
95 if self.templates.has_key(src) and \
96 stime < self.templates[src].mtime:
97 # compiled template is up to date
98 return self.templates[src]
100 # compile the template
101 self.templates[src] = pt = RoundupPageTemplate()
102 pt.write(open(src).read())
103 pt.id = filename
104 pt.mtime = time.time()
105 return pt
107 def __getitem__(self, name):
108 name, extension = os.path.splitext(name)
109 if extension:
110 extension = extension[1:]
111 try:
112 return self.get(name, extension)
113 except NoTemplate, message:
114 raise KeyError, message
116 class RoundupPageTemplate(PageTemplate.PageTemplate):
117 ''' A Roundup-specific PageTemplate.
119 Interrogate the client to set up the various template variables to
120 be available:
122 *context*
123 this is one of three things:
124 1. None - we're viewing a "home" page
125 2. The current class of item being displayed. This is an HTMLClass
126 instance.
127 3. The current item from the database, if we're viewing a specific
128 item, as an HTMLItem instance.
129 *request*
130 Includes information about the current request, including:
131 - the url
132 - the current index information (``filterspec``, ``filter`` args,
133 ``properties``, etc) parsed out of the form.
134 - methods for easy filterspec link generation
135 - *user*, the current user node as an HTMLItem instance
136 - *form*, the current CGI form information as a FieldStorage
137 *config*
138 The current tracker config.
139 *db*
140 The current database, used to access arbitrary database items.
141 *utils*
142 This is a special class that has its base in the TemplatingUtils
143 class in this file. If the tracker interfaces module defines a
144 TemplatingUtils class then it is mixed in, overriding the methods
145 in the base class.
146 '''
147 def getContext(self, client, classname, request):
148 # construct the TemplatingUtils class
149 utils = TemplatingUtils
150 if hasattr(client.instance.interfaces, 'TemplatingUtils'):
151 class utils(client.instance.interfaces.TemplatingUtils, utils):
152 pass
154 c = {
155 'options': {},
156 'nothing': None,
157 'request': request,
158 'db': HTMLDatabase(client),
159 'config': client.instance.config,
160 'tracker': client.instance,
161 'utils': utils(client),
162 'templates': Templates(client.instance.config.TEMPLATES),
163 }
164 # add in the item if there is one
165 if client.nodeid:
166 if classname == 'user':
167 c['context'] = HTMLUser(client, classname, client.nodeid,
168 anonymous=1)
169 else:
170 c['context'] = HTMLItem(client, classname, client.nodeid,
171 anonymous=1)
172 elif client.db.classes.has_key(classname):
173 c['context'] = HTMLClass(client, classname, anonymous=1)
174 return c
176 def render(self, client, classname, request, **options):
177 """Render this Page Template"""
179 if not self._v_cooked:
180 self._cook()
182 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
184 if self._v_errors:
185 raise PageTemplate.PTRuntimeError, \
186 'Page Template %s has errors.'%self.id
188 # figure the context
189 classname = classname or client.classname
190 request = request or HTMLRequest(client)
191 c = self.getContext(client, classname, request)
192 c.update({'options': options})
194 # and go
195 output = StringIO.StringIO()
196 TALInterpreter(self._v_program, self.macros,
197 getEngine().getContext(c), output, tal=1, strictinsert=0)()
198 return output.getvalue()
200 class HTMLDatabase:
201 ''' Return HTMLClasses for valid class fetches
202 '''
203 def __init__(self, client):
204 self._client = client
205 self._db = client.db
207 # we want config to be exposed
208 self.config = client.db.config
210 def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
211 # check to see if we're actually accessing an item
212 m = desre.match(item)
213 if m:
214 self._client.db.getclass(m.group('cl'))
215 return HTMLItem(self._client, m.group('cl'), m.group('id'))
216 else:
217 self._client.db.getclass(item)
218 return HTMLClass(self._client, item)
220 def __getattr__(self, attr):
221 try:
222 return self[attr]
223 except KeyError:
224 raise AttributeError, attr
226 def classes(self):
227 l = self._client.db.classes.keys()
228 l.sort()
229 return [HTMLClass(self._client, cn) for cn in l]
231 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
232 cl = db.getclass(prop.classname)
233 l = []
234 for entry in ids:
235 if num_re.match(entry):
236 l.append(entry)
237 else:
238 try:
239 l.append(cl.lookup(entry))
240 except KeyError:
241 # ignore invalid keys
242 pass
243 return l
245 class HTMLPermissions:
246 ''' Helpers that provide answers to commonly asked Permission questions.
247 '''
248 def is_edit_ok(self):
249 ''' Is the user allowed to Edit the current class?
250 '''
251 return self._db.security.hasPermission('Edit', self._client.userid,
252 self._classname)
253 def is_view_ok(self):
254 ''' Is the user allowed to View the current class?
255 '''
256 return self._db.security.hasPermission('View', self._client.userid,
257 self._classname)
258 def is_only_view_ok(self):
259 ''' Is the user only allowed to View (ie. not Edit) the current class?
260 '''
261 return self.is_view_ok() and not self.is_edit_ok()
263 class HTMLClass(HTMLPermissions):
264 ''' Accesses through a class (either through *class* or *db.<classname>*)
265 '''
266 def __init__(self, client, classname, anonymous=0):
267 self._client = client
268 self._db = client.db
269 self._anonymous = anonymous
271 # we want classname to be exposed, but _classname gives a
272 # consistent API for extending Class/Item
273 self._classname = self.classname = classname
274 self._klass = self._db.getclass(self.classname)
275 self._props = self._klass.getprops()
277 def __repr__(self):
278 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
280 def __getitem__(self, item):
281 ''' return an HTMLProperty instance
282 '''
283 #print 'HTMLClass.getitem', (self, item)
285 # we don't exist
286 if item == 'id':
287 return None
289 # get the property
290 prop = self._props[item]
292 # look up the correct HTMLProperty class
293 form = self._client.form
294 for klass, htmlklass in propclasses:
295 if not isinstance(prop, klass):
296 continue
297 if form.has_key(item):
298 if isinstance(prop, hyperdb.Multilink):
299 value = lookupIds(self._db, prop,
300 handleListCGIValue(form[item]))
301 elif isinstance(prop, hyperdb.Link):
302 value = form[item].value.strip()
303 if value:
304 value = lookupIds(self._db, prop, [value])[0]
305 else:
306 value = None
307 else:
308 value = form[item].value.strip() or None
309 else:
310 if isinstance(prop, hyperdb.Multilink):
311 value = []
312 else:
313 value = None
314 return htmlklass(self._client, self._classname, '', prop, item,
315 value, self._anonymous)
317 # no good
318 raise KeyError, item
320 def __getattr__(self, attr):
321 ''' convenience access '''
322 try:
323 return self[attr]
324 except KeyError:
325 raise AttributeError, attr
327 def getItem(self, itemid, num_re=re.compile('\d+')):
328 ''' Get an item of this class by its item id.
329 '''
330 # make sure we're looking at an itemid
331 if not num_re.match(itemid):
332 itemid = self._klass.lookup(itemid)
334 if self.classname == 'user':
335 klass = HTMLUser
336 else:
337 klass = HTMLItem
339 return klass(self._client, self.classname, itemid)
341 def properties(self):
342 ''' Return HTMLProperty for all of this class' properties.
343 '''
344 l = []
345 for name, prop in self._props.items():
346 for klass, htmlklass in propclasses:
347 if isinstance(prop, hyperdb.Multilink):
348 value = []
349 else:
350 value = None
351 if isinstance(prop, klass):
352 l.append(htmlklass(self._client, self._classname, '',
353 prop, name, value, self._anonymous))
354 return l
356 def list(self):
357 ''' List all items in this class.
358 '''
359 if self.classname == 'user':
360 klass = HTMLUser
361 else:
362 klass = HTMLItem
364 # get the list and sort it nicely
365 l = self._klass.list()
366 sortfunc = make_sort_function(self._db, self.classname)
367 l.sort(sortfunc)
369 l = [klass(self._client, self.classname, x) for x in l]
370 return l
372 def csv(self):
373 ''' Return the items of this class as a chunk of CSV text.
374 '''
375 # get the CSV module
376 try:
377 import csv
378 except ImportError:
379 return 'Sorry, you need the csv module to use this function.\n'\
380 'Get it from: http://www.object-craft.com.au/projects/csv/'
382 props = self.propnames()
383 p = csv.parser()
384 s = StringIO.StringIO()
385 s.write(p.join(props) + '\n')
386 for nodeid in self._klass.list():
387 l = []
388 for name in props:
389 value = self._klass.get(nodeid, name)
390 if value is None:
391 l.append('')
392 elif isinstance(value, type([])):
393 l.append(':'.join(map(str, value)))
394 else:
395 l.append(str(self._klass.get(nodeid, name)))
396 s.write(p.join(l) + '\n')
397 return s.getvalue()
399 def propnames(self):
400 ''' Return the list of the names of the properties of this class.
401 '''
402 idlessprops = self._klass.getprops(protected=0).keys()
403 idlessprops.sort()
404 return ['id'] + idlessprops
406 def filter(self, request=None):
407 ''' Return a list of items from this class, filtered and sorted
408 by the current requested filterspec/filter/sort/group args
409 '''
410 # XXX allow direct specification of the filterspec etc.
411 if request is not None:
412 filterspec = request.filterspec
413 sort = request.sort
414 group = request.group
415 else:
416 filterspec = {}
417 sort = (None,None)
418 group = (None,None)
419 if self.classname == 'user':
420 klass = HTMLUser
421 else:
422 klass = HTMLItem
423 l = [klass(self._client, self.classname, x)
424 for x in self._klass.filter(None, filterspec, sort, group)]
425 return l
427 def classhelp(self, properties=None, label='(list)', width='500',
428 height='400', property=''):
429 ''' Pop up a javascript window with class help
431 This generates a link to a popup window which displays the
432 properties indicated by "properties" of the class named by
433 "classname". The "properties" should be a comma-separated list
434 (eg. 'id,name,description'). Properties defaults to all the
435 properties of a class (excluding id, creator, created and
436 activity).
438 You may optionally override the label displayed, the width and
439 height. The popup window will be resizable and scrollable.
441 If the "property" arg is given, it's passed through to the
442 javascript help_window function.
443 '''
444 if properties is None:
445 properties = self._klass.getprops(protected=0).keys()
446 properties.sort()
447 properties = ','.join(properties)
448 if property:
449 property = '&property=%s'%property
450 return '<a class="classhelp" href="javascript:help_window(\'%s?:'\
451 'template=help&properties=%s%s\', \'%s\', \'%s\')">%s</a>'%(
452 self.classname, properties, property, width, height, label)
454 def submit(self, label="Submit New Entry"):
455 ''' Generate a submit button (and action hidden element)
456 '''
457 return ' <input type="hidden" name=":action" value="new">\n'\
458 ' <input type="submit" name="submit" value="%s">'%label
460 def history(self):
461 return 'New node - no history'
463 def renderWith(self, name, **kwargs):
464 ''' Render this class with the given template.
465 '''
466 # create a new request and override the specified args
467 req = HTMLRequest(self._client)
468 req.classname = self.classname
469 req.update(kwargs)
471 # new template, using the specified classname and request
472 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
474 # use our fabricated request
475 return pt.render(self._client, self.classname, req)
477 class HTMLItem(HTMLPermissions):
478 ''' Accesses through an *item*
479 '''
480 def __init__(self, client, classname, nodeid, anonymous=0):
481 self._client = client
482 self._db = client.db
483 self._classname = classname
484 self._nodeid = nodeid
485 self._klass = self._db.getclass(classname)
486 self._props = self._klass.getprops()
488 # do we prefix the form items with the item's identification?
489 self._anonymous = anonymous
491 def __repr__(self):
492 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
493 self._nodeid)
495 def __getitem__(self, item):
496 ''' return an HTMLProperty instance
497 '''
498 #print 'HTMLItem.getitem', (self, item)
499 if item == 'id':
500 return self._nodeid
502 # get the property
503 prop = self._props[item]
505 # get the value, handling missing values
506 value = None
507 if int(self._nodeid) > 0:
508 value = self._klass.get(self._nodeid, item, None)
509 if value is None:
510 if isinstance(self._props[item], hyperdb.Multilink):
511 value = []
513 # look up the correct HTMLProperty class
514 for klass, htmlklass in propclasses:
515 if isinstance(prop, klass):
516 return htmlklass(self._client, self._classname,
517 self._nodeid, prop, item, value, self._anonymous)
519 raise KeyError, item
521 def __getattr__(self, attr):
522 ''' convenience access to properties '''
523 try:
524 return self[attr]
525 except KeyError:
526 raise AttributeError, attr
528 def submit(self, label="Submit Changes"):
529 ''' Generate a submit button (and action hidden element)
530 '''
531 return ' <input type="hidden" name=":action" value="edit">\n'\
532 ' <input type="submit" name="submit" value="%s">'%label
534 def journal(self, direction='descending'):
535 ''' Return a list of HTMLJournalEntry instances.
536 '''
537 # XXX do this
538 return []
540 def history(self, direction='descending', dre=re.compile('\d+')):
541 l = ['<table class="history">'
542 '<tr><th colspan="4" class="header">',
543 _('History'),
544 '</th></tr><tr>',
545 _('<th>Date</th>'),
546 _('<th>User</th>'),
547 _('<th>Action</th>'),
548 _('<th>Args</th>'),
549 '</tr>']
550 current = {}
551 comments = {}
552 history = self._klass.history(self._nodeid)
553 history.sort()
554 timezone = self._db.getUserTimezone()
555 if direction == 'descending':
556 history.reverse()
557 for prop_n in self._props.keys():
558 prop = self[prop_n]
559 if isinstance(prop, HTMLProperty):
560 current[prop_n] = prop.plain()
561 # make link if hrefable
562 if (self._props.has_key(prop_n) and
563 isinstance(self._props[prop_n], hyperdb.Link)):
564 classname = self._props[prop_n].classname
565 if os.path.exists(os.path.join(self._db.config.TEMPLATES, classname + '.item')):
566 current[prop_n] = '<a href="%s%s">%s</a>'%(classname,
567 self._klass.get(self._nodeid, prop_n, None), current[prop_n])
569 for id, evt_date, user, action, args in history:
570 date_s = str(evt_date.local(timezone)).replace("."," ")
571 arg_s = ''
572 if action == 'link' and type(args) == type(()):
573 if len(args) == 3:
574 linkcl, linkid, key = args
575 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
576 linkcl, linkid, key)
577 else:
578 arg_s = str(args)
580 elif action == 'unlink' and type(args) == type(()):
581 if len(args) == 3:
582 linkcl, linkid, key = args
583 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
584 linkcl, linkid, key)
585 else:
586 arg_s = str(args)
588 elif type(args) == type({}):
589 cell = []
590 for k in args.keys():
591 # try to get the relevant property and treat it
592 # specially
593 try:
594 prop = self._props[k]
595 except KeyError:
596 prop = None
597 if prop is not None:
598 if args[k] and (isinstance(prop, hyperdb.Multilink) or
599 isinstance(prop, hyperdb.Link)):
600 # figure what the link class is
601 classname = prop.classname
602 try:
603 linkcl = self._db.getclass(classname)
604 except KeyError:
605 labelprop = None
606 comments[classname] = _('''The linked class
607 %(classname)s no longer exists''')%locals()
608 labelprop = linkcl.labelprop(1)
609 hrefable = os.path.exists(
610 os.path.join(self._db.config.TEMPLATES,
611 classname+'.item'))
613 if isinstance(prop, hyperdb.Multilink) and args[k]:
614 ml = []
615 for linkid in args[k]:
616 if isinstance(linkid, type(())):
617 sublabel = linkid[0] + ' '
618 linkids = linkid[1]
619 else:
620 sublabel = ''
621 linkids = [linkid]
622 subml = []
623 for linkid in linkids:
624 label = classname + linkid
625 # if we have a label property, try to use it
626 # TODO: test for node existence even when
627 # there's no labelprop!
628 try:
629 if labelprop is not None and \
630 labelprop != 'id':
631 label = linkcl.get(linkid, labelprop)
632 except IndexError:
633 comments['no_link'] = _('''<strike>The
634 linked node no longer
635 exists</strike>''')
636 subml.append('<strike>%s</strike>'%label)
637 else:
638 if hrefable:
639 subml.append('<a href="%s%s">%s</a>'%(
640 classname, linkid, label))
641 else:
642 subml.append(label)
643 ml.append(sublabel + ', '.join(subml))
644 cell.append('%s:\n %s'%(k, ', '.join(ml)))
645 elif isinstance(prop, hyperdb.Link) and args[k]:
646 label = classname + args[k]
647 # if we have a label property, try to use it
648 # TODO: test for node existence even when
649 # there's no labelprop!
650 if labelprop is not None and labelprop != 'id':
651 try:
652 label = linkcl.get(args[k], labelprop)
653 except IndexError:
654 comments['no_link'] = _('''<strike>The
655 linked node no longer
656 exists</strike>''')
657 cell.append(' <strike>%s</strike>,\n'%label)
658 # "flag" this is done .... euwww
659 label = None
660 if label is not None:
661 if hrefable:
662 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
663 else:
664 old = label;
665 cell.append('%s: %s' % (k,old))
666 if current.has_key(k):
667 cell[-1] += ' -> %s'%current[k]
668 current[k] = old
670 elif isinstance(prop, hyperdb.Date) and args[k]:
671 d = date.Date(args[k]).local(timezone)
672 cell.append('%s: %s'%(k, str(d)))
673 if current.has_key(k):
674 cell[-1] += ' -> %s' % current[k]
675 current[k] = str(d)
677 elif isinstance(prop, hyperdb.Interval) and args[k]:
678 d = date.Interval(args[k])
679 cell.append('%s: %s'%(k, str(d)))
680 if current.has_key(k):
681 cell[-1] += ' -> %s'%current[k]
682 current[k] = str(d)
684 elif isinstance(prop, hyperdb.String) and args[k]:
685 cell.append('%s: %s'%(k, cgi.escape(args[k])))
686 if current.has_key(k):
687 cell[-1] += ' -> %s'%current[k]
688 current[k] = cgi.escape(args[k])
690 elif not args[k]:
691 if current.has_key(k):
692 cell.append('%s: %s'%(k, current[k]))
693 current[k] = '(no value)'
694 else:
695 cell.append('%s: (no value)'%k)
697 else:
698 cell.append('%s: %s'%(k, str(args[k])))
699 if current.has_key(k):
700 cell[-1] += ' -> %s'%current[k]
701 current[k] = str(args[k])
702 else:
703 # property no longer exists
704 comments['no_exist'] = _('''<em>The indicated property
705 no longer exists</em>''')
706 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
707 arg_s = '<br />'.join(cell)
708 else:
709 # unkown event!!
710 comments['unknown'] = _('''<strong><em>This event is not
711 handled by the history display!</em></strong>''')
712 arg_s = '<strong><em>' + str(args) + '</em></strong>'
713 date_s = date_s.replace(' ', ' ')
714 # if the user's an itemid, figure the username (older journals
715 # have the username)
716 if dre.match(user):
717 user = self._db.user.get(user, 'username')
718 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
719 date_s, user, action, arg_s))
720 if comments:
721 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
722 for entry in comments.values():
723 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
724 l.append('</table>')
725 return '\n'.join(l)
727 def renderQueryForm(self):
728 ''' Render this item, which is a query, as a search form.
729 '''
730 # create a new request and override the specified args
731 req = HTMLRequest(self._client)
732 req.classname = self._klass.get(self._nodeid, 'klass')
733 name = self._klass.get(self._nodeid, 'name')
734 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
735 '&:queryname=%s'%urllib.quote(name))
737 # new template, using the specified classname and request
738 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
740 # use our fabricated request
741 return pt.render(self._client, req.classname, req)
743 class HTMLUser(HTMLItem):
744 ''' Accesses through the *user* (a special case of item)
745 '''
746 def __init__(self, client, classname, nodeid, anonymous=0):
747 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
748 self._default_classname = client.classname
750 # used for security checks
751 self._security = client.db.security
753 _marker = []
754 def hasPermission(self, permission, classname=_marker):
755 ''' Determine if the user has the Permission.
757 The class being tested defaults to the template's class, but may
758 be overidden for this test by suppling an alternate classname.
759 '''
760 if classname is self._marker:
761 classname = self._default_classname
762 return self._security.hasPermission(permission, self._nodeid, classname)
764 def is_edit_ok(self):
765 ''' Is the user allowed to Edit the current class?
766 Also check whether this is the current user's info.
767 '''
768 return self._db.security.hasPermission('Edit', self._client.userid,
769 self._classname) or self._nodeid == self._client.userid
771 def is_view_ok(self):
772 ''' Is the user allowed to View the current class?
773 Also check whether this is the current user's info.
774 '''
775 return self._db.security.hasPermission('Edit', self._client.userid,
776 self._classname) or self._nodeid == self._client.userid
778 class HTMLProperty:
779 ''' String, Number, Date, Interval HTMLProperty
781 Has useful attributes:
783 _name the name of the property
784 _value the value of the property if any
786 A wrapper object which may be stringified for the plain() behaviour.
787 '''
788 def __init__(self, client, classname, nodeid, prop, name, value,
789 anonymous=0):
790 self._client = client
791 self._db = client.db
792 self._classname = classname
793 self._nodeid = nodeid
794 self._prop = prop
795 self._value = value
796 self._anonymous = anonymous
797 self._name = name
798 if not anonymous:
799 self._formname = '%s%s@%s'%(classname, nodeid, name)
800 else:
801 self._formname = name
802 def __repr__(self):
803 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
804 self._prop, self._value)
805 def __str__(self):
806 return self.plain()
807 def __cmp__(self, other):
808 if isinstance(other, HTMLProperty):
809 return cmp(self._value, other._value)
810 return cmp(self._value, other)
812 class StringHTMLProperty(HTMLProperty):
813 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
814 r'(?P<email>[\w\.]+@[\w\.\-]+)|'
815 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
816 def _hyper_repl(self, match):
817 if match.group('url'):
818 s = match.group('url')
819 return '<a href="%s">%s</a>'%(s, s)
820 elif match.group('email'):
821 s = match.group('email')
822 return '<a href="mailto:%s">%s</a>'%(s, s)
823 else:
824 s = match.group('item')
825 s1 = match.group('class')
826 s2 = match.group('id')
827 try:
828 # make sure s1 is a valid tracker classname
829 self._db.getclass(s1)
830 return '<a href="%s">%s %s</a>'%(s, s1, s2)
831 except KeyError:
832 return '%s%s'%(s1, s2)
834 def plain(self, escape=0, hyperlink=0):
835 ''' Render a "plain" representation of the property
837 "escape" turns on/off HTML quoting
838 "hyperlink" turns on/off in-text hyperlinking of URLs, email
839 addresses and designators
840 '''
841 if self._value is None:
842 return ''
843 if escape:
844 s = cgi.escape(str(self._value))
845 else:
846 s = str(self._value)
847 if hyperlink:
848 if not escape:
849 s = cgi.escape(s)
850 s = self.hyper_re.sub(self._hyper_repl, s)
851 return s
853 def stext(self, escape=0):
854 ''' Render the value of the property as StructuredText.
856 This requires the StructureText module to be installed separately.
857 '''
858 s = self.plain(escape=escape)
859 if not StructuredText:
860 return s
861 return StructuredText(s,level=1,header=0)
863 def field(self, size = 30):
864 ''' Render a form edit field for the property
865 '''
866 if self._value is None:
867 value = ''
868 else:
869 value = cgi.escape(str(self._value))
870 value = '"'.join(value.split('"'))
871 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
873 def multiline(self, escape=0, rows=5, cols=40):
874 ''' Render a multiline form edit field for the property
875 '''
876 if self._value is None:
877 value = ''
878 else:
879 value = cgi.escape(str(self._value))
880 value = '"'.join(value.split('"'))
881 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
882 self._formname, rows, cols, value)
884 def email(self, escape=1):
885 ''' Render the value of the property as an obscured email address
886 '''
887 if self._value is None: value = ''
888 else: value = str(self._value)
889 if value.find('@') != -1:
890 name, domain = value.split('@')
891 domain = ' '.join(domain.split('.')[:-1])
892 name = name.replace('.', ' ')
893 value = '%s at %s ...'%(name, domain)
894 else:
895 value = value.replace('.', ' ')
896 if escape:
897 value = cgi.escape(value)
898 return value
900 class PasswordHTMLProperty(HTMLProperty):
901 def plain(self):
902 ''' Render a "plain" representation of the property
903 '''
904 if self._value is None:
905 return ''
906 return _('*encrypted*')
908 def field(self, size = 30):
909 ''' Render a form edit field for the property.
910 '''
911 return '<input type="password" name="%s" size="%s">'%(self._formname, size)
913 def confirm(self, size = 30):
914 ''' Render a second form edit field for the property, used for
915 confirmation that the user typed the password correctly. Generates
916 a field with name ":confirm:name".
917 '''
918 return '<input type="password" name=":confirm:%s" size="%s">'%(
919 self._formname, size)
921 class NumberHTMLProperty(HTMLProperty):
922 def plain(self):
923 ''' Render a "plain" representation of the property
924 '''
925 return str(self._value)
927 def field(self, size = 30):
928 ''' Render a form edit field for the property
929 '''
930 if self._value is None:
931 value = ''
932 else:
933 value = cgi.escape(str(self._value))
934 value = '"'.join(value.split('"'))
935 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
937 def __int__(self):
938 ''' Return an int of me
939 '''
940 return int(self._value)
942 def __float__(self):
943 ''' Return a float of me
944 '''
945 return float(self._value)
948 class BooleanHTMLProperty(HTMLProperty):
949 def plain(self):
950 ''' Render a "plain" representation of the property
951 '''
952 if self._value is None:
953 return ''
954 return self._value and "Yes" or "No"
956 def field(self):
957 ''' Render a form edit field for the property
958 '''
959 checked = self._value and "checked" or ""
960 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
961 checked)
962 if checked:
963 checked = ""
964 else:
965 checked = "checked"
966 s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
967 checked)
968 return s
970 class DateHTMLProperty(HTMLProperty):
971 def plain(self):
972 ''' Render a "plain" representation of the property
973 '''
974 if self._value is None:
975 return ''
976 return str(self._value.local(self._db.getUserTimezone()))
978 def now(self):
979 ''' Return the current time.
981 This is useful for defaulting a new value. Returns a
982 DateHTMLProperty.
983 '''
984 return DateHTMLProperty(self._client, self._nodeid, self._prop,
985 self._formname, date.Date('.'))
987 def field(self, size = 30):
988 ''' Render a form edit field for the property
989 '''
990 if self._value is None:
991 value = ''
992 else:
993 value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
994 value = '"'.join(value.split('"'))
995 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
997 def reldate(self, pretty=1):
998 ''' Render the interval between the date and now.
1000 If the "pretty" flag is true, then make the display pretty.
1001 '''
1002 if not self._value:
1003 return ''
1005 # figure the interval
1006 interval = date.Date('.') - self._value
1007 if pretty:
1008 return interval.pretty()
1009 return str(interval)
1011 _marker = []
1012 def pretty(self, format=_marker):
1013 ''' Render the date in a pretty format (eg. month names, spaces).
1015 The format string is a standard python strftime format string.
1016 Note that if the day is zero, and appears at the start of the
1017 string, then it'll be stripped from the output. This is handy
1018 for the situatin when a date only specifies a month and a year.
1019 '''
1020 if format is not self._marker:
1021 return self._value.pretty(format)
1022 else:
1023 return self._value.pretty()
1025 def local(self, offset):
1026 ''' Return the date/time as a local (timezone offset) date/time.
1027 '''
1028 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1029 self._formname, self._value.local(offset))
1031 class IntervalHTMLProperty(HTMLProperty):
1032 def plain(self):
1033 ''' Render a "plain" representation of the property
1034 '''
1035 if self._value is None:
1036 return ''
1037 return str(self._value)
1039 def pretty(self):
1040 ''' Render the interval in a pretty format (eg. "yesterday")
1041 '''
1042 return self._value.pretty()
1044 def field(self, size = 30):
1045 ''' Render a form edit field for the property
1046 '''
1047 if self._value is None:
1048 value = ''
1049 else:
1050 value = cgi.escape(str(self._value))
1051 value = '"'.join(value.split('"'))
1052 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1054 class LinkHTMLProperty(HTMLProperty):
1055 ''' Link HTMLProperty
1056 Include the above as well as being able to access the class
1057 information. Stringifying the object itself results in the value
1058 from the item being displayed. Accessing attributes of this object
1059 result in the appropriate entry from the class being queried for the
1060 property accessed (so item/assignedto/name would look up the user
1061 entry identified by the assignedto property on item, and then the
1062 name property of that user)
1063 '''
1064 def __init__(self, *args, **kw):
1065 HTMLProperty.__init__(self, *args, **kw)
1066 # if we're representing a form value, then the -1 from the form really
1067 # should be a None
1068 if str(self._value) == '-1':
1069 self._value = None
1071 def __getattr__(self, attr):
1072 ''' return a new HTMLItem '''
1073 #print 'Link.getattr', (self, attr, self._value)
1074 if not self._value:
1075 raise AttributeError, "Can't access missing value"
1076 if self._prop.classname == 'user':
1077 klass = HTMLUser
1078 else:
1079 klass = HTMLItem
1080 i = klass(self._client, self._prop.classname, self._value)
1081 return getattr(i, attr)
1083 def plain(self, escape=0):
1084 ''' Render a "plain" representation of the property
1085 '''
1086 if self._value is None:
1087 return ''
1088 linkcl = self._db.classes[self._prop.classname]
1089 k = linkcl.labelprop(1)
1090 value = str(linkcl.get(self._value, k))
1091 if escape:
1092 value = cgi.escape(value)
1093 return value
1095 def field(self, showid=0, size=None):
1096 ''' Render a form edit field for the property
1097 '''
1098 linkcl = self._db.getclass(self._prop.classname)
1099 if linkcl.getprops().has_key('order'):
1100 sort_on = 'order'
1101 else:
1102 sort_on = linkcl.labelprop()
1103 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1104 # TODO: make this a field display, not a menu one!
1105 l = ['<select name="%s">'%self._formname]
1106 k = linkcl.labelprop(1)
1107 if self._value is None:
1108 s = 'selected '
1109 else:
1110 s = ''
1111 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1113 # make sure we list the current value if it's retired
1114 if self._value and self._value not in options:
1115 options.insert(0, self._value)
1117 for optionid in options:
1118 # get the option value, and if it's None use an empty string
1119 option = linkcl.get(optionid, k) or ''
1121 # figure if this option is selected
1122 s = ''
1123 if optionid == self._value:
1124 s = 'selected '
1126 # figure the label
1127 if showid:
1128 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1129 else:
1130 lab = option
1132 # truncate if it's too long
1133 if size is not None and len(lab) > size:
1134 lab = lab[:size-3] + '...'
1136 # and generate
1137 lab = cgi.escape(lab)
1138 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1139 l.append('</select>')
1140 return '\n'.join(l)
1142 def menu(self, size=None, height=None, showid=0, additional=[],
1143 **conditions):
1144 ''' Render a form select list for this property
1145 '''
1146 value = self._value
1148 # sort function
1149 sortfunc = make_sort_function(self._db, self._prop.classname)
1151 linkcl = self._db.getclass(self._prop.classname)
1152 l = ['<select name="%s">'%self._formname]
1153 k = linkcl.labelprop(1)
1154 s = ''
1155 if value is None:
1156 s = 'selected '
1157 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1158 if linkcl.getprops().has_key('order'):
1159 sort_on = ('+', 'order')
1160 else:
1161 sort_on = ('+', linkcl.labelprop())
1162 options = linkcl.filter(None, conditions, sort_on, (None, None))
1164 # make sure we list the current value if it's retired
1165 if self._value and self._value not in options:
1166 options.insert(0, self._value)
1168 for optionid in options:
1169 # get the option value, and if it's None use an empty string
1170 option = linkcl.get(optionid, k) or ''
1172 # figure if this option is selected
1173 s = ''
1174 if value in [optionid, option]:
1175 s = 'selected '
1177 # figure the label
1178 if showid:
1179 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1180 else:
1181 lab = option
1183 # truncate if it's too long
1184 if size is not None and len(lab) > size:
1185 lab = lab[:size-3] + '...'
1186 if additional:
1187 m = []
1188 for propname in additional:
1189 m.append(linkcl.get(optionid, propname))
1190 lab = lab + ' (%s)'%', '.join(map(str, m))
1192 # and generate
1193 lab = cgi.escape(lab)
1194 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1195 l.append('</select>')
1196 return '\n'.join(l)
1197 # def checklist(self, ...)
1199 class MultilinkHTMLProperty(HTMLProperty):
1200 ''' Multilink HTMLProperty
1202 Also be iterable, returning a wrapper object like the Link case for
1203 each entry in the multilink.
1204 '''
1205 def __len__(self):
1206 ''' length of the multilink '''
1207 return len(self._value)
1209 def __getattr__(self, attr):
1210 ''' no extended attribute accesses make sense here '''
1211 raise AttributeError, attr
1213 def __getitem__(self, num):
1214 ''' iterate and return a new HTMLItem
1215 '''
1216 #print 'Multi.getitem', (self, num)
1217 value = self._value[num]
1218 if self._prop.classname == 'user':
1219 klass = HTMLUser
1220 else:
1221 klass = HTMLItem
1222 return klass(self._client, self._prop.classname, value)
1224 def __contains__(self, value):
1225 ''' Support the "in" operator. We have to make sure the passed-in
1226 value is a string first, not a *HTMLProperty.
1227 '''
1228 return str(value) in self._value
1230 def reverse(self):
1231 ''' return the list in reverse order
1232 '''
1233 l = self._value[:]
1234 l.reverse()
1235 if self._prop.classname == 'user':
1236 klass = HTMLUser
1237 else:
1238 klass = HTMLItem
1239 return [klass(self._client, self._prop.classname, value) for value in l]
1241 def plain(self, escape=0):
1242 ''' Render a "plain" representation of the property
1243 '''
1244 linkcl = self._db.classes[self._prop.classname]
1245 k = linkcl.labelprop(1)
1246 labels = []
1247 for v in self._value:
1248 labels.append(linkcl.get(v, k))
1249 value = ', '.join(labels)
1250 if escape:
1251 value = cgi.escape(value)
1252 return value
1254 def field(self, size=30, showid=0):
1255 ''' Render a form edit field for the property
1256 '''
1257 sortfunc = make_sort_function(self._db, self._prop.classname)
1258 linkcl = self._db.getclass(self._prop.classname)
1259 value = self._value[:]
1260 if value:
1261 value.sort(sortfunc)
1262 # map the id to the label property
1263 if not linkcl.getkey():
1264 showid=1
1265 if not showid:
1266 k = linkcl.labelprop(1)
1267 value = [linkcl.get(v, k) for v in value]
1268 value = cgi.escape(','.join(value))
1269 return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1271 def menu(self, size=None, height=None, showid=0, additional=[],
1272 **conditions):
1273 ''' Render a form select list for this property
1274 '''
1275 value = self._value
1277 # sort function
1278 sortfunc = make_sort_function(self._db, self._prop.classname)
1280 linkcl = self._db.getclass(self._prop.classname)
1281 if linkcl.getprops().has_key('order'):
1282 sort_on = ('+', 'order')
1283 else:
1284 sort_on = ('+', linkcl.labelprop())
1285 options = linkcl.filter(None, conditions, sort_on, (None,None))
1286 height = height or min(len(options), 7)
1287 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1288 k = linkcl.labelprop(1)
1290 # make sure we list the current values if they're retired
1291 for val in value:
1292 if val not in options:
1293 options.insert(0, val)
1295 for optionid in options:
1296 # get the option value, and if it's None use an empty string
1297 option = linkcl.get(optionid, k) or ''
1299 # figure if this option is selected
1300 s = ''
1301 if optionid in value or option in value:
1302 s = 'selected '
1304 # figure the label
1305 if showid:
1306 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1307 else:
1308 lab = option
1309 # truncate if it's too long
1310 if size is not None and len(lab) > size:
1311 lab = lab[:size-3] + '...'
1312 if additional:
1313 m = []
1314 for propname in additional:
1315 m.append(linkcl.get(optionid, propname))
1316 lab = lab + ' (%s)'%', '.join(m)
1318 # and generate
1319 lab = cgi.escape(lab)
1320 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1321 lab))
1322 l.append('</select>')
1323 return '\n'.join(l)
1325 # set the propclasses for HTMLItem
1326 propclasses = (
1327 (hyperdb.String, StringHTMLProperty),
1328 (hyperdb.Number, NumberHTMLProperty),
1329 (hyperdb.Boolean, BooleanHTMLProperty),
1330 (hyperdb.Date, DateHTMLProperty),
1331 (hyperdb.Interval, IntervalHTMLProperty),
1332 (hyperdb.Password, PasswordHTMLProperty),
1333 (hyperdb.Link, LinkHTMLProperty),
1334 (hyperdb.Multilink, MultilinkHTMLProperty),
1335 )
1337 def make_sort_function(db, classname):
1338 '''Make a sort function for a given class
1339 '''
1340 linkcl = db.getclass(classname)
1341 if linkcl.getprops().has_key('order'):
1342 sort_on = 'order'
1343 else:
1344 sort_on = linkcl.labelprop()
1345 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1346 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1347 return sortfunc
1349 def handleListCGIValue(value):
1350 ''' Value is either a single item or a list of items. Each item has a
1351 .value that we're actually interested in.
1352 '''
1353 if isinstance(value, type([])):
1354 return [value.value for value in value]
1355 else:
1356 value = value.value.strip()
1357 if not value:
1358 return []
1359 return value.split(',')
1361 class ShowDict:
1362 ''' A convenience access to the :columns index parameters
1363 '''
1364 def __init__(self, columns):
1365 self.columns = {}
1366 for col in columns:
1367 self.columns[col] = 1
1368 def __getitem__(self, name):
1369 return self.columns.has_key(name)
1371 class HTMLRequest:
1372 ''' The *request*, holding the CGI form and environment.
1374 "form" the CGI form as a cgi.FieldStorage
1375 "env" the CGI environment variables
1376 "base" the base URL for this instance
1377 "user" a HTMLUser instance for this user
1378 "classname" the current classname (possibly None)
1379 "template" the current template (suffix, also possibly None)
1381 Index args:
1382 "columns" dictionary of the columns to display in an index page
1383 "show" a convenience access to columns - request/show/colname will
1384 be true if the columns should be displayed, false otherwise
1385 "sort" index sort column (direction, column name)
1386 "group" index grouping property (direction, column name)
1387 "filter" properties to filter the index on
1388 "filterspec" values to filter the index on
1389 "search_text" text to perform a full-text search on for an index
1391 '''
1392 def __init__(self, client):
1393 self.client = client
1395 # easier access vars
1396 self.form = client.form
1397 self.env = client.env
1398 self.base = client.base
1399 self.user = HTMLUser(client, 'user', client.userid)
1401 # store the current class name and action
1402 self.classname = client.classname
1403 self.template = client.template
1405 # the special char to use for special vars
1406 self.special_char = '@'
1408 self._post_init()
1410 def _post_init(self):
1411 ''' Set attributes based on self.form
1412 '''
1413 # extract the index display information from the form
1414 self.columns = []
1415 for name in ':columns @columns'.split():
1416 if self.form.has_key(name):
1417 self.special_char = name[0]
1418 self.columns = handleListCGIValue(self.form[name])
1419 break
1420 self.show = ShowDict(self.columns)
1422 # sorting
1423 self.sort = (None, None)
1424 for name in ':sort @sort'.split():
1425 if self.form.has_key(name):
1426 self.special_char = name[0]
1427 sort = self.form[name].value
1428 if sort.startswith('-'):
1429 self.sort = ('-', sort[1:])
1430 else:
1431 self.sort = ('+', sort)
1432 if self.form.has_key(self.special_char+'sortdir'):
1433 self.sort = ('-', self.sort[1])
1435 # grouping
1436 self.group = (None, None)
1437 for name in ':group @group'.split():
1438 if self.form.has_key(name):
1439 self.special_char = name[0]
1440 group = self.form[name].value
1441 if group.startswith('-'):
1442 self.group = ('-', group[1:])
1443 else:
1444 self.group = ('+', group)
1445 if self.form.has_key(self.special_char+'groupdir'):
1446 self.group = ('-', self.group[1])
1448 # filtering
1449 self.filter = []
1450 for name in ':filter @filter'.split():
1451 if self.form.has_key(name):
1452 self.special_char = name[0]
1453 self.filter = handleListCGIValue(self.form[name])
1455 self.filterspec = {}
1456 db = self.client.db
1457 if self.classname is not None:
1458 props = db.getclass(self.classname).getprops()
1459 for name in self.filter:
1460 if self.form.has_key(name):
1461 prop = props[name]
1462 fv = self.form[name]
1463 if (isinstance(prop, hyperdb.Link) or
1464 isinstance(prop, hyperdb.Multilink)):
1465 self.filterspec[name] = lookupIds(db, prop,
1466 handleListCGIValue(fv))
1467 else:
1468 self.filterspec[name] = fv.value
1470 # full-text search argument
1471 self.search_text = None
1472 for name in ':search_text @search_text'.split():
1473 if self.form.has_key(name):
1474 self.special_char = name[0]
1475 self.search_text = self.form[name].value
1477 # pagination - size and start index
1478 # figure batch args
1479 self.pagesize = 50
1480 for name in ':pagesize @pagesize'.split():
1481 if self.form.has_key(name):
1482 self.special_char = name[0]
1483 self.pagesize = int(self.form[name].value)
1485 self.startwith = 0
1486 for name in ':startwith @startwith'.split():
1487 if self.form.has_key(name):
1488 self.special_char = name[0]
1489 self.startwith = int(self.form[name].value)
1491 def updateFromURL(self, url):
1492 ''' Parse the URL for query args, and update my attributes using the
1493 values.
1494 '''
1495 self.form = {}
1496 for name, value in cgi.parse_qsl(url):
1497 if self.form.has_key(name):
1498 if isinstance(self.form[name], type([])):
1499 self.form[name].append(cgi.MiniFieldStorage(name, value))
1500 else:
1501 self.form[name] = [self.form[name],
1502 cgi.MiniFieldStorage(name, value)]
1503 else:
1504 self.form[name] = cgi.MiniFieldStorage(name, value)
1505 self._post_init()
1507 def update(self, kwargs):
1508 ''' Update my attributes using the keyword args
1509 '''
1510 self.__dict__.update(kwargs)
1511 if kwargs.has_key('columns'):
1512 self.show = ShowDict(self.columns)
1514 def description(self):
1515 ''' Return a description of the request - handle for the page title.
1516 '''
1517 s = [self.client.db.config.TRACKER_NAME]
1518 if self.classname:
1519 if self.client.nodeid:
1520 s.append('- %s%s'%(self.classname, self.client.nodeid))
1521 else:
1522 if self.template == 'item':
1523 s.append('- new %s'%self.classname)
1524 elif self.template == 'index':
1525 s.append('- %s index'%self.classname)
1526 else:
1527 s.append('- %s %s'%(self.classname, self.template))
1528 else:
1529 s.append('- home')
1530 return ' '.join(s)
1532 def __str__(self):
1533 d = {}
1534 d.update(self.__dict__)
1535 f = ''
1536 for k in self.form.keys():
1537 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1538 d['form'] = f
1539 e = ''
1540 for k,v in self.env.items():
1541 e += '\n %r=%r'%(k, v)
1542 d['env'] = e
1543 return '''
1544 form: %(form)s
1545 base: %(base)r
1546 classname: %(classname)r
1547 template: %(template)r
1548 columns: %(columns)r
1549 sort: %(sort)r
1550 group: %(group)r
1551 filter: %(filter)r
1552 search_text: %(search_text)r
1553 pagesize: %(pagesize)r
1554 startwith: %(startwith)r
1555 env: %(env)s
1556 '''%d
1558 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1559 filterspec=1):
1560 ''' return the current index args as form elements '''
1561 l = []
1562 sc = self.special_char
1563 s = '<input type="hidden" name="%s" value="%s">'
1564 if columns and self.columns:
1565 l.append(s%(sc+'columns', ','.join(self.columns)))
1566 if sort and self.sort[1] is not None:
1567 if self.sort[0] == '-':
1568 val = '-'+self.sort[1]
1569 else:
1570 val = self.sort[1]
1571 l.append(s%(sc+'sort', val))
1572 if group and self.group[1] is not None:
1573 if self.group[0] == '-':
1574 val = '-'+self.group[1]
1575 else:
1576 val = self.group[1]
1577 l.append(s%(sc+'group', val))
1578 if filter and self.filter:
1579 l.append(s%(sc+'filter', ','.join(self.filter)))
1580 if filterspec:
1581 for k,v in self.filterspec.items():
1582 if type(v) == type([]):
1583 l.append(s%(k, ','.join(v)))
1584 else:
1585 l.append(s%(k, v))
1586 if self.search_text:
1587 l.append(s%(sc+'search_text', self.search_text))
1588 l.append(s%(sc+'pagesize', self.pagesize))
1589 l.append(s%(sc+'startwith', self.startwith))
1590 return '\n'.join(l)
1592 def indexargs_url(self, url, args):
1593 ''' Embed the current index args in a URL
1594 '''
1595 sc = self.special_char
1596 l = ['%s=%s'%(k,v) for k,v in args.items()]
1598 # pull out the special values (prefixed by @ or :)
1599 specials = {}
1600 for key in args.keys():
1601 if key[0] in '@:':
1602 specials[key[1:]] = args[key]
1604 # ok, now handle the specials we received in the request
1605 if self.columns and not specials.has_key('columns'):
1606 l.append(sc+'columns=%s'%(','.join(self.columns)))
1607 if self.sort[1] is not None and not specials.has_key('sort'):
1608 if self.sort[0] == '-':
1609 val = '-'+self.sort[1]
1610 else:
1611 val = self.sort[1]
1612 l.append(sc+'sort=%s'%val)
1613 if self.group[1] is not None and not specials.has_key('group'):
1614 if self.group[0] == '-':
1615 val = '-'+self.group[1]
1616 else:
1617 val = self.group[1]
1618 l.append(sc+'group=%s'%val)
1619 if self.filter and not specials.has_key('filter'):
1620 l.append(sc+'filter=%s'%(','.join(self.filter)))
1621 if self.search_text and not specials.has_key('search_text'):
1622 l.append(sc+'search_text=%s'%self.search_text)
1623 if not specials.has_key('pagesize'):
1624 l.append(sc+'pagesize=%s'%self.pagesize)
1625 if not specials.has_key('startwith'):
1626 l.append(sc+'startwith=%s'%self.startwith)
1628 # finally, the remainder of the filter args in the request
1629 for k,v in self.filterspec.items():
1630 if not args.has_key(k):
1631 if type(v) == type([]):
1632 l.append('%s=%s'%(k, ','.join(v)))
1633 else:
1634 l.append('%s=%s'%(k, v))
1635 return '%s?%s'%(url, '&'.join(l))
1636 indexargs_href = indexargs_url
1638 def base_javascript(self):
1639 return '''
1640 <script language="javascript">
1641 submitted = false;
1642 function submit_once() {
1643 if (submitted) {
1644 alert("Your request is being processed.\\nPlease be patient.");
1645 return 0;
1646 }
1647 submitted = true;
1648 return 1;
1649 }
1651 function help_window(helpurl, width, height) {
1652 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1653 }
1654 </script>
1655 '''%self.base
1657 def batch(self):
1658 ''' Return a batch object for results from the "current search"
1659 '''
1660 filterspec = self.filterspec
1661 sort = self.sort
1662 group = self.group
1664 # get the list of ids we're batching over
1665 klass = self.client.db.getclass(self.classname)
1666 if self.search_text:
1667 matches = self.client.db.indexer.search(
1668 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1669 else:
1670 matches = None
1671 l = klass.filter(matches, filterspec, sort, group)
1673 # return the batch object, using IDs only
1674 return Batch(self.client, l, self.pagesize, self.startwith,
1675 classname=self.classname)
1677 # extend the standard ZTUtils Batch object to remove dependency on
1678 # Acquisition and add a couple of useful methods
1679 class Batch(ZTUtils.Batch):
1680 ''' Use me to turn a list of items, or item ids of a given class, into a
1681 series of batches.
1683 ========= ========================================================
1684 Parameter Usage
1685 ========= ========================================================
1686 sequence a list of HTMLItems or item ids
1687 classname if sequence is a list of ids, this is the class of item
1688 size how big to make the sequence.
1689 start where to start (0-indexed) in the sequence.
1690 end where to end (0-indexed) in the sequence.
1691 orphan if the next batch would contain less items than this
1692 value, then it is combined with this batch
1693 overlap the number of items shared between adjacent batches
1694 ========= ========================================================
1696 Attributes: Note that the "start" attribute, unlike the
1697 argument, is a 1-based index (I know, lame). "first" is the
1698 0-based index. "length" is the actual number of elements in
1699 the batch.
1701 "sequence_length" is the length of the original, unbatched, sequence.
1702 '''
1703 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1704 overlap=0, classname=None):
1705 self.client = client
1706 self.last_index = self.last_item = None
1707 self.current_item = None
1708 self.classname = classname
1709 self.sequence_length = len(sequence)
1710 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1711 overlap)
1713 # overwrite so we can late-instantiate the HTMLItem instance
1714 def __getitem__(self, index):
1715 if index < 0:
1716 if index + self.end < self.first: raise IndexError, index
1717 return self._sequence[index + self.end]
1719 if index >= self.length:
1720 raise IndexError, index
1722 # move the last_item along - but only if the fetched index changes
1723 # (for some reason, index 0 is fetched twice)
1724 if index != self.last_index:
1725 self.last_item = self.current_item
1726 self.last_index = index
1728 item = self._sequence[index + self.first]
1729 if self.classname:
1730 # map the item ids to instances
1731 if self.classname == 'user':
1732 item = HTMLUser(self.client, self.classname, item)
1733 else:
1734 item = HTMLItem(self.client, self.classname, item)
1735 self.current_item = item
1736 return item
1738 def propchanged(self, property):
1739 ''' Detect if the property marked as being the group property
1740 changed in the last iteration fetch
1741 '''
1742 if (self.last_item is None or
1743 self.last_item[property] != self.current_item[property]):
1744 return 1
1745 return 0
1747 # override these 'cos we don't have access to acquisition
1748 def previous(self):
1749 if self.start == 1:
1750 return None
1751 return Batch(self.client, self._sequence, self._size,
1752 self.first - self._size + self.overlap, 0, self.orphan,
1753 self.overlap)
1755 def next(self):
1756 try:
1757 self._sequence[self.end]
1758 except IndexError:
1759 return None
1760 return Batch(self.client, self._sequence, self._size,
1761 self.end - self.overlap, 0, self.orphan, self.overlap)
1763 class TemplatingUtils:
1764 ''' Utilities for templating
1765 '''
1766 def __init__(self, client):
1767 self.client = client
1768 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1769 return Batch(self.client, sequence, size, start, end, orphan,
1770 overlap)