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 not self.form.has_key(name):
1461 continue
1462 prop = props[name]
1463 fv = self.form[name]
1464 if (isinstance(prop, hyperdb.Link) or
1465 isinstance(prop, hyperdb.Multilink)):
1466 self.filterspec[name] = lookupIds(db, prop,
1467 handleListCGIValue(fv))
1468 else:
1469 if isinstance(fv, type([])):
1470 self.filterspec[name] = [v.value for v in fv]
1471 else:
1472 self.filterspec[name] = fv.value
1474 # full-text search argument
1475 self.search_text = None
1476 for name in ':search_text @search_text'.split():
1477 if self.form.has_key(name):
1478 self.special_char = name[0]
1479 self.search_text = self.form[name].value
1481 # pagination - size and start index
1482 # figure batch args
1483 self.pagesize = 50
1484 for name in ':pagesize @pagesize'.split():
1485 if self.form.has_key(name):
1486 self.special_char = name[0]
1487 self.pagesize = int(self.form[name].value)
1489 self.startwith = 0
1490 for name in ':startwith @startwith'.split():
1491 if self.form.has_key(name):
1492 self.special_char = name[0]
1493 self.startwith = int(self.form[name].value)
1495 def updateFromURL(self, url):
1496 ''' Parse the URL for query args, and update my attributes using the
1497 values.
1498 '''
1499 self.form = {}
1500 for name, value in cgi.parse_qsl(url):
1501 if self.form.has_key(name):
1502 if isinstance(self.form[name], type([])):
1503 self.form[name].append(cgi.MiniFieldStorage(name, value))
1504 else:
1505 self.form[name] = [self.form[name],
1506 cgi.MiniFieldStorage(name, value)]
1507 else:
1508 self.form[name] = cgi.MiniFieldStorage(name, value)
1509 self._post_init()
1511 def update(self, kwargs):
1512 ''' Update my attributes using the keyword args
1513 '''
1514 self.__dict__.update(kwargs)
1515 if kwargs.has_key('columns'):
1516 self.show = ShowDict(self.columns)
1518 def description(self):
1519 ''' Return a description of the request - handle for the page title.
1520 '''
1521 s = [self.client.db.config.TRACKER_NAME]
1522 if self.classname:
1523 if self.client.nodeid:
1524 s.append('- %s%s'%(self.classname, self.client.nodeid))
1525 else:
1526 if self.template == 'item':
1527 s.append('- new %s'%self.classname)
1528 elif self.template == 'index':
1529 s.append('- %s index'%self.classname)
1530 else:
1531 s.append('- %s %s'%(self.classname, self.template))
1532 else:
1533 s.append('- home')
1534 return ' '.join(s)
1536 def __str__(self):
1537 d = {}
1538 d.update(self.__dict__)
1539 f = ''
1540 for k in self.form.keys():
1541 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1542 d['form'] = f
1543 e = ''
1544 for k,v in self.env.items():
1545 e += '\n %r=%r'%(k, v)
1546 d['env'] = e
1547 return '''
1548 form: %(form)s
1549 base: %(base)r
1550 classname: %(classname)r
1551 template: %(template)r
1552 columns: %(columns)r
1553 sort: %(sort)r
1554 group: %(group)r
1555 filter: %(filter)r
1556 search_text: %(search_text)r
1557 pagesize: %(pagesize)r
1558 startwith: %(startwith)r
1559 env: %(env)s
1560 '''%d
1562 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1563 filterspec=1):
1564 ''' return the current index args as form elements '''
1565 l = []
1566 sc = self.special_char
1567 s = '<input type="hidden" name="%s" value="%s">'
1568 if columns and self.columns:
1569 l.append(s%(sc+'columns', ','.join(self.columns)))
1570 if sort and self.sort[1] is not None:
1571 if self.sort[0] == '-':
1572 val = '-'+self.sort[1]
1573 else:
1574 val = self.sort[1]
1575 l.append(s%(sc+'sort', val))
1576 if group and self.group[1] is not None:
1577 if self.group[0] == '-':
1578 val = '-'+self.group[1]
1579 else:
1580 val = self.group[1]
1581 l.append(s%(sc+'group', val))
1582 if filter and self.filter:
1583 l.append(s%(sc+'filter', ','.join(self.filter)))
1584 if filterspec:
1585 for k,v in self.filterspec.items():
1586 if type(v) == type([]):
1587 l.append(s%(k, ','.join(v)))
1588 else:
1589 l.append(s%(k, v))
1590 if self.search_text:
1591 l.append(s%(sc+'search_text', self.search_text))
1592 l.append(s%(sc+'pagesize', self.pagesize))
1593 l.append(s%(sc+'startwith', self.startwith))
1594 return '\n'.join(l)
1596 def indexargs_url(self, url, args):
1597 ''' Embed the current index args in a URL
1598 '''
1599 sc = self.special_char
1600 l = ['%s=%s'%(k,v) for k,v in args.items()]
1602 # pull out the special values (prefixed by @ or :)
1603 specials = {}
1604 for key in args.keys():
1605 if key[0] in '@:':
1606 specials[key[1:]] = args[key]
1608 # ok, now handle the specials we received in the request
1609 if self.columns and not specials.has_key('columns'):
1610 l.append(sc+'columns=%s'%(','.join(self.columns)))
1611 if self.sort[1] is not None and not specials.has_key('sort'):
1612 if self.sort[0] == '-':
1613 val = '-'+self.sort[1]
1614 else:
1615 val = self.sort[1]
1616 l.append(sc+'sort=%s'%val)
1617 if self.group[1] is not None and not specials.has_key('group'):
1618 if self.group[0] == '-':
1619 val = '-'+self.group[1]
1620 else:
1621 val = self.group[1]
1622 l.append(sc+'group=%s'%val)
1623 if self.filter and not specials.has_key('filter'):
1624 l.append(sc+'filter=%s'%(','.join(self.filter)))
1625 if self.search_text and not specials.has_key('search_text'):
1626 l.append(sc+'search_text=%s'%self.search_text)
1627 if not specials.has_key('pagesize'):
1628 l.append(sc+'pagesize=%s'%self.pagesize)
1629 if not specials.has_key('startwith'):
1630 l.append(sc+'startwith=%s'%self.startwith)
1632 # finally, the remainder of the filter args in the request
1633 for k,v in self.filterspec.items():
1634 if not args.has_key(k):
1635 if type(v) == type([]):
1636 l.append('%s=%s'%(k, ','.join(v)))
1637 else:
1638 l.append('%s=%s'%(k, v))
1639 return '%s?%s'%(url, '&'.join(l))
1640 indexargs_href = indexargs_url
1642 def base_javascript(self):
1643 return '''
1644 <script language="javascript">
1645 submitted = false;
1646 function submit_once() {
1647 if (submitted) {
1648 alert("Your request is being processed.\\nPlease be patient.");
1649 return 0;
1650 }
1651 submitted = true;
1652 return 1;
1653 }
1655 function help_window(helpurl, width, height) {
1656 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1657 }
1658 </script>
1659 '''%self.base
1661 def batch(self):
1662 ''' Return a batch object for results from the "current search"
1663 '''
1664 filterspec = self.filterspec
1665 sort = self.sort
1666 group = self.group
1668 # get the list of ids we're batching over
1669 klass = self.client.db.getclass(self.classname)
1670 if self.search_text:
1671 matches = self.client.db.indexer.search(
1672 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1673 else:
1674 matches = None
1675 l = klass.filter(matches, filterspec, sort, group)
1677 # return the batch object, using IDs only
1678 return Batch(self.client, l, self.pagesize, self.startwith,
1679 classname=self.classname)
1681 # extend the standard ZTUtils Batch object to remove dependency on
1682 # Acquisition and add a couple of useful methods
1683 class Batch(ZTUtils.Batch):
1684 ''' Use me to turn a list of items, or item ids of a given class, into a
1685 series of batches.
1687 ========= ========================================================
1688 Parameter Usage
1689 ========= ========================================================
1690 sequence a list of HTMLItems or item ids
1691 classname if sequence is a list of ids, this is the class of item
1692 size how big to make the sequence.
1693 start where to start (0-indexed) in the sequence.
1694 end where to end (0-indexed) in the sequence.
1695 orphan if the next batch would contain less items than this
1696 value, then it is combined with this batch
1697 overlap the number of items shared between adjacent batches
1698 ========= ========================================================
1700 Attributes: Note that the "start" attribute, unlike the
1701 argument, is a 1-based index (I know, lame). "first" is the
1702 0-based index. "length" is the actual number of elements in
1703 the batch.
1705 "sequence_length" is the length of the original, unbatched, sequence.
1706 '''
1707 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1708 overlap=0, classname=None):
1709 self.client = client
1710 self.last_index = self.last_item = None
1711 self.current_item = None
1712 self.classname = classname
1713 self.sequence_length = len(sequence)
1714 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1715 overlap)
1717 # overwrite so we can late-instantiate the HTMLItem instance
1718 def __getitem__(self, index):
1719 if index < 0:
1720 if index + self.end < self.first: raise IndexError, index
1721 return self._sequence[index + self.end]
1723 if index >= self.length:
1724 raise IndexError, index
1726 # move the last_item along - but only if the fetched index changes
1727 # (for some reason, index 0 is fetched twice)
1728 if index != self.last_index:
1729 self.last_item = self.current_item
1730 self.last_index = index
1732 item = self._sequence[index + self.first]
1733 if self.classname:
1734 # map the item ids to instances
1735 if self.classname == 'user':
1736 item = HTMLUser(self.client, self.classname, item)
1737 else:
1738 item = HTMLItem(self.client, self.classname, item)
1739 self.current_item = item
1740 return item
1742 def propchanged(self, property):
1743 ''' Detect if the property marked as being the group property
1744 changed in the last iteration fetch
1745 '''
1746 if (self.last_item is None or
1747 self.last_item[property] != self.current_item[property]):
1748 return 1
1749 return 0
1751 # override these 'cos we don't have access to acquisition
1752 def previous(self):
1753 if self.start == 1:
1754 return None
1755 return Batch(self.client, self._sequence, self._size,
1756 self.first - self._size + self.overlap, 0, self.orphan,
1757 self.overlap)
1759 def next(self):
1760 try:
1761 self._sequence[self.end]
1762 except IndexError:
1763 return None
1764 return Batch(self.client, self._sequence, self._size,
1765 self.end - self.overlap, 0, self.orphan, self.overlap)
1767 class TemplatingUtils:
1768 ''' Utilities for templating
1769 '''
1770 def __init__(self, client):
1771 self.client = client
1772 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1773 return Batch(self.client, sequence, size, start, end, orphan,
1774 overlap)