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