1 from __future__ import nested_scopes
3 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
5 from roundup import hyperdb, date, rcsv
6 from roundup.i18n import _
8 try:
9 import cPickle as pickle
10 except ImportError:
11 import pickle
12 try:
13 import cStringIO as StringIO
14 except ImportError:
15 import StringIO
16 try:
17 import StructuredText
18 except ImportError:
19 StructuredText = None
21 # bring in the templating support
22 from roundup.cgi.PageTemplates import PageTemplate
23 from roundup.cgi.PageTemplates.Expressions import getEngine
24 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
25 from roundup.cgi import ZTUtils
27 class NoTemplate(Exception):
28 pass
30 def find_template(dir, name, extension):
31 ''' Find a template in the nominated dir
32 '''
33 # find the source
34 if extension:
35 filename = '%s.%s'%(name, extension)
36 else:
37 filename = name
39 # try old-style
40 src = os.path.join(dir, filename)
41 if os.path.exists(src):
42 return (src, filename)
44 # try with a .html extension (new-style)
45 filename = filename + '.html'
46 src = os.path.join(dir, filename)
47 if os.path.exists(src):
48 return (src, filename)
50 # no extension == no generic template is possible
51 if not extension:
52 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
54 # try for a _generic template
55 generic = '_generic.%s'%extension
56 src = os.path.join(dir, generic)
57 if os.path.exists(src):
58 return (src, generic)
60 # finally, try _generic.html
61 generic = generic + '.html'
62 src = os.path.join(dir, generic)
63 if os.path.exists(src):
64 return (src, generic)
66 raise NoTemplate, 'No template file exists for templating "%s" '\
67 'with template "%s" (neither "%s" nor "%s")'%(name, extension,
68 filename, generic)
70 class Templates:
71 templates = {}
73 def __init__(self, dir):
74 self.dir = dir
76 def precompileTemplates(self):
77 ''' Go through a directory and precompile all the templates therein
78 '''
79 for filename in os.listdir(self.dir):
80 if os.path.isdir(filename): continue
81 if '.' in filename:
82 name, extension = filename.split('.')
83 self.get(name, extension)
84 else:
85 self.get(filename, None)
87 def get(self, name, extension=None):
88 ''' Interface to get a template, possibly loading a compiled template.
90 "name" and "extension" indicate the template we're after, which in
91 most cases will be "name.extension". If "extension" is None, then
92 we look for a template just called "name" with no extension.
94 If the file "name.extension" doesn't exist, we look for
95 "_generic.extension" as a fallback.
96 '''
97 # default the name to "home"
98 if name is None:
99 name = 'home'
100 elif extension is None and '.' in name:
101 # split name
102 name, extension = name.split('.')
104 # find the source
105 src, filename = find_template(self.dir, name, extension)
107 # has it changed?
108 try:
109 stime = os.stat(src)[os.path.stat.ST_MTIME]
110 except os.error, error:
111 if error.errno != errno.ENOENT:
112 raise
114 if self.templates.has_key(src) and \
115 stime < self.templates[src].mtime:
116 # compiled template is up to date
117 return self.templates[src]
119 # compile the template
120 self.templates[src] = pt = RoundupPageTemplate()
121 # use pt_edit so we can pass the content_type guess too
122 content_type = mimetypes.guess_type(filename)[0] or 'text/html'
123 pt.pt_edit(open(src).read(), content_type)
124 pt.id = filename
125 pt.mtime = time.time()
126 return pt
128 def __getitem__(self, name):
129 name, extension = os.path.splitext(name)
130 if extension:
131 extension = extension[1:]
132 try:
133 return self.get(name, extension)
134 except NoTemplate, message:
135 raise KeyError, message
137 class RoundupPageTemplate(PageTemplate.PageTemplate):
138 ''' A Roundup-specific PageTemplate.
140 Interrogate the client to set up the various template variables to
141 be available:
143 *context*
144 this is one of three things:
145 1. None - we're viewing a "home" page
146 2. The current class of item being displayed. This is an HTMLClass
147 instance.
148 3. The current item from the database, if we're viewing a specific
149 item, as an HTMLItem instance.
150 *request*
151 Includes information about the current request, including:
152 - the url
153 - the current index information (``filterspec``, ``filter`` args,
154 ``properties``, etc) parsed out of the form.
155 - methods for easy filterspec link generation
156 - *user*, the current user node as an HTMLItem instance
157 - *form*, the current CGI form information as a FieldStorage
158 *config*
159 The current tracker config.
160 *db*
161 The current database, used to access arbitrary database items.
162 *utils*
163 This is a special class that has its base in the TemplatingUtils
164 class in this file. If the tracker interfaces module defines a
165 TemplatingUtils class then it is mixed in, overriding the methods
166 in the base class.
167 '''
168 def getContext(self, client, classname, request):
169 # construct the TemplatingUtils class
170 utils = TemplatingUtils
171 if hasattr(client.instance.interfaces, 'TemplatingUtils'):
172 class utils(client.instance.interfaces.TemplatingUtils, utils):
173 pass
175 c = {
176 'options': {},
177 'nothing': None,
178 'request': request,
179 'db': HTMLDatabase(client),
180 'config': client.instance.config,
181 'tracker': client.instance,
182 'utils': utils(client),
183 'templates': Templates(client.instance.config.TEMPLATES),
184 }
185 # add in the item if there is one
186 if client.nodeid:
187 if classname == 'user':
188 c['context'] = HTMLUser(client, classname, client.nodeid,
189 anonymous=1)
190 else:
191 c['context'] = HTMLItem(client, classname, client.nodeid,
192 anonymous=1)
193 elif client.db.classes.has_key(classname):
194 c['context'] = HTMLClass(client, classname, anonymous=1)
195 return c
197 def render(self, client, classname, request, **options):
198 """Render this Page Template"""
200 if not self._v_cooked:
201 self._cook()
203 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
205 if self._v_errors:
206 raise PageTemplate.PTRuntimeError, \
207 'Page Template %s has errors.'%self.id
209 # figure the context
210 classname = classname or client.classname
211 request = request or HTMLRequest(client)
212 c = self.getContext(client, classname, request)
213 c.update({'options': options})
215 # and go
216 output = StringIO.StringIO()
217 TALInterpreter(self._v_program, self.macros,
218 getEngine().getContext(c), output, tal=1, strictinsert=0)()
219 return output.getvalue()
221 class HTMLDatabase:
222 ''' Return HTMLClasses for valid class fetches
223 '''
224 def __init__(self, client):
225 self._client = client
226 self._db = client.db
228 # we want config to be exposed
229 self.config = client.db.config
231 def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
232 # check to see if we're actually accessing an item
233 m = desre.match(item)
234 if m:
235 self._client.db.getclass(m.group('cl'))
236 return HTMLItem(self._client, m.group('cl'), m.group('id'))
237 else:
238 self._client.db.getclass(item)
239 return HTMLClass(self._client, item)
241 def __getattr__(self, attr):
242 try:
243 return self[attr]
244 except KeyError:
245 raise AttributeError, attr
247 def classes(self):
248 l = self._client.db.classes.keys()
249 l.sort()
250 return [HTMLClass(self._client, cn) for cn in l]
252 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
253 cl = db.getclass(prop.classname)
254 l = []
255 for entry in ids:
256 if num_re.match(entry):
257 l.append(entry)
258 else:
259 try:
260 l.append(cl.lookup(entry))
261 except KeyError:
262 # ignore invalid keys
263 pass
264 return l
266 class HTMLPermissions:
267 ''' Helpers that provide answers to commonly asked Permission questions.
268 '''
269 def is_edit_ok(self):
270 ''' Is the user allowed to Edit the current class?
271 '''
272 return self._db.security.hasPermission('Edit', self._client.userid,
273 self._classname)
274 def is_view_ok(self):
275 ''' Is the user allowed to View the current class?
276 '''
277 return self._db.security.hasPermission('View', self._client.userid,
278 self._classname)
279 def is_only_view_ok(self):
280 ''' Is the user only allowed to View (ie. not Edit) the current class?
281 '''
282 return self.is_view_ok() and not self.is_edit_ok()
284 class HTMLClass(HTMLPermissions):
285 ''' Accesses through a class (either through *class* or *db.<classname>*)
286 '''
287 def __init__(self, client, classname, anonymous=0):
288 self._client = client
289 self._db = client.db
290 self._anonymous = anonymous
292 # we want classname to be exposed, but _classname gives a
293 # consistent API for extending Class/Item
294 self._classname = self.classname = classname
295 self._klass = self._db.getclass(self.classname)
296 self._props = self._klass.getprops()
298 def __repr__(self):
299 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
301 def __getitem__(self, item):
302 ''' return an HTMLProperty instance
303 '''
304 #print 'HTMLClass.getitem', (self, item)
306 # we don't exist
307 if item == 'id':
308 return None
310 # get the property
311 prop = self._props[item]
313 # look up the correct HTMLProperty class
314 form = self._client.form
315 for klass, htmlklass in propclasses:
316 if not isinstance(prop, klass):
317 continue
318 if form.has_key(item):
319 if isinstance(prop, hyperdb.Multilink):
320 value = lookupIds(self._db, prop,
321 handleListCGIValue(form[item]))
322 elif isinstance(prop, hyperdb.Link):
323 value = form[item].value.strip()
324 if value:
325 value = lookupIds(self._db, prop, [value])[0]
326 else:
327 value = None
328 else:
329 value = form[item].value.strip() or None
330 else:
331 if isinstance(prop, hyperdb.Multilink):
332 value = []
333 else:
334 value = None
335 return htmlklass(self._client, self._classname, '', prop, item,
336 value, self._anonymous)
338 # no good
339 raise KeyError, item
341 def __getattr__(self, attr):
342 ''' convenience access '''
343 try:
344 return self[attr]
345 except KeyError:
346 raise AttributeError, attr
348 def designator(self):
349 ''' Return this class' designator (classname) '''
350 return self._classname
352 def getItem(self, itemid, num_re=re.compile('-?\d+')):
353 ''' Get an item of this class by its item id.
354 '''
355 # make sure we're looking at an itemid
356 if not num_re.match(itemid):
357 itemid = self._klass.lookup(itemid)
359 if self.classname == 'user':
360 klass = HTMLUser
361 else:
362 klass = HTMLItem
364 return klass(self._client, self.classname, itemid)
366 def properties(self, sort=1):
367 ''' Return HTMLProperty for all of this class' properties.
368 '''
369 l = []
370 for name, prop in self._props.items():
371 for klass, htmlklass in propclasses:
372 if isinstance(prop, hyperdb.Multilink):
373 value = []
374 else:
375 value = None
376 if isinstance(prop, klass):
377 l.append(htmlklass(self._client, self._classname, '',
378 prop, name, value, self._anonymous))
379 if sort:
380 l.sort(lambda a,b:cmp(a._name, b._name))
381 return l
383 def list(self):
384 ''' List all items in this class.
385 '''
386 if self.classname == 'user':
387 klass = HTMLUser
388 else:
389 klass = HTMLItem
391 # get the list and sort it nicely
392 l = self._klass.list()
393 sortfunc = make_sort_function(self._db, self.classname)
394 l.sort(sortfunc)
396 l = [klass(self._client, self.classname, x) for x in l]
397 return l
399 def csv(self):
400 ''' Return the items of this class as a chunk of CSV text.
401 '''
402 if rcsv.error:
403 return rcsv.error
405 props = self.propnames()
406 s = StringIO.StringIO()
407 writer = rcsv.writer(s, rcsv.comma_separated)
408 writer.writerow(props)
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 writer.writerow(l)
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 template = find_template(self._db.config.TEMPLATES,
595 classname, 'item')
596 if template[1].startswith('_generic'):
597 raise NoTemplate, 'not really...'
598 except NoTemplate:
599 pass
600 else:
601 id = self._klass.get(self._nodeid, prop_n, None)
602 current[prop_n] = '<a href="%s%s">%s</a>'%(
603 classname, id, current[prop_n])
605 for id, evt_date, user, action, args in history:
606 date_s = str(evt_date.local(timezone)).replace("."," ")
607 arg_s = ''
608 if action == 'link' and type(args) == type(()):
609 if len(args) == 3:
610 linkcl, linkid, key = args
611 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
612 linkcl, linkid, key)
613 else:
614 arg_s = str(args)
616 elif action == 'unlink' and type(args) == type(()):
617 if len(args) == 3:
618 linkcl, linkid, key = args
619 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
620 linkcl, linkid, key)
621 else:
622 arg_s = str(args)
624 elif type(args) == type({}):
625 cell = []
626 for k in args.keys():
627 # try to get the relevant property and treat it
628 # specially
629 try:
630 prop = self._props[k]
631 except KeyError:
632 prop = None
633 if prop is None:
634 # property no longer exists
635 comments['no_exist'] = _('''<em>The indicated property
636 no longer exists</em>''')
637 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
638 continue
640 if args[k] and (isinstance(prop, hyperdb.Multilink) or
641 isinstance(prop, hyperdb.Link)):
642 # figure what the link class is
643 classname = prop.classname
644 try:
645 linkcl = self._db.getclass(classname)
646 except KeyError:
647 labelprop = None
648 comments[classname] = _('''The linked class
649 %(classname)s no longer exists''')%locals()
650 labelprop = linkcl.labelprop(1)
651 try:
652 template = find_template(self._db.config.TEMPLATES,
653 classname, 'item')
654 if template[1].startswith('_generic'):
655 raise NoTemplate, 'not really...'
656 hrefable = 1
657 except NoTemplate:
658 hrefable = 0
660 if isinstance(prop, hyperdb.Multilink) and args[k]:
661 ml = []
662 for linkid in args[k]:
663 if isinstance(linkid, type(())):
664 sublabel = linkid[0] + ' '
665 linkids = linkid[1]
666 else:
667 sublabel = ''
668 linkids = [linkid]
669 subml = []
670 for linkid in linkids:
671 label = classname + linkid
672 # if we have a label property, try to use it
673 # TODO: test for node existence even when
674 # there's no labelprop!
675 try:
676 if labelprop is not None and \
677 labelprop != 'id':
678 label = linkcl.get(linkid, labelprop)
679 except IndexError:
680 comments['no_link'] = _('''<strike>The
681 linked node no longer
682 exists</strike>''')
683 subml.append('<strike>%s</strike>'%label)
684 else:
685 if hrefable:
686 subml.append('<a href="%s%s">%s</a>'%(
687 classname, linkid, label))
688 else:
689 subml.append(label)
690 ml.append(sublabel + ', '.join(subml))
691 cell.append('%s:\n %s'%(k, ', '.join(ml)))
692 elif isinstance(prop, hyperdb.Link) and args[k]:
693 label = classname + args[k]
694 # if we have a label property, try to use it
695 # TODO: test for node existence even when
696 # there's no labelprop!
697 if labelprop is not None and labelprop != 'id':
698 try:
699 label = linkcl.get(args[k], labelprop)
700 except IndexError:
701 comments['no_link'] = _('''<strike>The
702 linked node no longer
703 exists</strike>''')
704 cell.append(' <strike>%s</strike>,\n'%label)
705 # "flag" this is done .... euwww
706 label = None
707 if label is not None:
708 if hrefable:
709 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
710 else:
711 old = label;
712 cell.append('%s: %s' % (k,old))
713 if current.has_key(k):
714 cell[-1] += ' -> %s'%current[k]
715 current[k] = old
717 elif isinstance(prop, hyperdb.Date) and args[k]:
718 d = date.Date(args[k]).local(timezone)
719 cell.append('%s: %s'%(k, str(d)))
720 if current.has_key(k):
721 cell[-1] += ' -> %s' % current[k]
722 current[k] = str(d)
724 elif isinstance(prop, hyperdb.Interval) and args[k]:
725 d = date.Interval(args[k])
726 cell.append('%s: %s'%(k, str(d)))
727 if current.has_key(k):
728 cell[-1] += ' -> %s'%current[k]
729 current[k] = str(d)
731 elif isinstance(prop, hyperdb.String) and args[k]:
732 cell.append('%s: %s'%(k, cgi.escape(args[k])))
733 if current.has_key(k):
734 cell[-1] += ' -> %s'%current[k]
735 current[k] = cgi.escape(args[k])
737 elif not args[k]:
738 if current.has_key(k):
739 cell.append('%s: %s'%(k, current[k]))
740 current[k] = '(no value)'
741 else:
742 cell.append('%s: (no value)'%k)
744 else:
745 cell.append('%s: %s'%(k, str(args[k])))
746 if current.has_key(k):
747 cell[-1] += ' -> %s'%current[k]
748 current[k] = str(args[k])
750 arg_s = '<br />'.join(cell)
751 else:
752 # unkown event!!
753 comments['unknown'] = _('''<strong><em>This event is not
754 handled by the history display!</em></strong>''')
755 arg_s = '<strong><em>' + str(args) + '</em></strong>'
756 date_s = date_s.replace(' ', ' ')
757 # if the user's an itemid, figure the username (older journals
758 # have the username)
759 if dre.match(user):
760 user = self._db.user.get(user, 'username')
761 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
762 date_s, user, action, arg_s))
763 if comments:
764 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
765 for entry in comments.values():
766 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
767 l.append('</table>')
768 return '\n'.join(l)
770 def renderQueryForm(self):
771 ''' Render this item, which is a query, as a search form.
772 '''
773 # create a new request and override the specified args
774 req = HTMLRequest(self._client)
775 req.classname = self._klass.get(self._nodeid, 'klass')
776 name = self._klass.get(self._nodeid, 'name')
777 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
778 '&@queryname=%s'%urllib.quote(name))
780 # new template, using the specified classname and request
781 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
783 # use our fabricated request
784 return pt.render(self._client, req.classname, req)
786 class HTMLUser(HTMLItem):
787 ''' Accesses through the *user* (a special case of item)
788 '''
789 def __init__(self, client, classname, nodeid, anonymous=0):
790 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
791 self._default_classname = client.classname
793 # used for security checks
794 self._security = client.db.security
796 _marker = []
797 def hasPermission(self, permission, classname=_marker):
798 ''' Determine if the user has the Permission.
800 The class being tested defaults to the template's class, but may
801 be overidden for this test by suppling an alternate classname.
802 '''
803 if classname is self._marker:
804 classname = self._default_classname
805 return self._security.hasPermission(permission, self._nodeid, classname)
807 def is_edit_ok(self):
808 ''' Is the user allowed to Edit the current class?
809 Also check whether this is the current user's info.
810 '''
811 return self._db.security.hasPermission('Edit', self._client.userid,
812 self._classname) or (self._nodeid == self._client.userid and
813 self._db.user.get(self._client.userid, 'username') != 'anonymous')
815 def is_view_ok(self):
816 ''' Is the user allowed to View the current class?
817 Also check whether this is the current user's info.
818 '''
819 return self._db.security.hasPermission('Edit', self._client.userid,
820 self._classname) or (self._nodeid == self._client.userid and
821 self._db.user.get(self._client.userid, 'username') != 'anonymous')
823 class HTMLProperty:
824 ''' String, Number, Date, Interval HTMLProperty
826 Has useful attributes:
828 _name the name of the property
829 _value the value of the property if any
831 A wrapper object which may be stringified for the plain() behaviour.
832 '''
833 def __init__(self, client, classname, nodeid, prop, name, value,
834 anonymous=0):
835 self._client = client
836 self._db = client.db
837 self._classname = classname
838 self._nodeid = nodeid
839 self._prop = prop
840 self._value = value
841 self._anonymous = anonymous
842 self._name = name
843 if not anonymous:
844 self._formname = '%s%s@%s'%(classname, nodeid, name)
845 else:
846 self._formname = name
847 def __repr__(self):
848 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
849 self._prop, self._value)
850 def __str__(self):
851 return self.plain()
852 def __cmp__(self, other):
853 if isinstance(other, HTMLProperty):
854 return cmp(self._value, other._value)
855 return cmp(self._value, other)
857 class StringHTMLProperty(HTMLProperty):
858 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
859 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
860 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
861 def _hyper_repl(self, match):
862 if match.group('url'):
863 s = match.group('url')
864 return '<a href="%s">%s</a>'%(s, s)
865 elif match.group('email'):
866 s = match.group('email')
867 return '<a href="mailto:%s">%s</a>'%(s, s)
868 else:
869 s = match.group('item')
870 s1 = match.group('class')
871 s2 = match.group('id')
872 try:
873 # make sure s1 is a valid tracker classname
874 self._db.getclass(s1)
875 return '<a href="%s">%s %s</a>'%(s, s1, s2)
876 except KeyError:
877 return '%s%s'%(s1, s2)
879 def hyperlinked(self):
880 ''' Render a "hyperlinked" version of the text '''
881 return self.plain(hyperlink=1)
883 def plain(self, escape=0, hyperlink=0):
884 ''' Render a "plain" representation of the property
886 "escape" turns on/off HTML quoting
887 "hyperlink" turns on/off in-text hyperlinking of URLs, email
888 addresses and designators
889 '''
890 if self._value is None:
891 return ''
892 if escape:
893 s = cgi.escape(str(self._value))
894 else:
895 s = str(self._value)
896 if hyperlink:
897 # no, we *must* escape this text
898 if not escape:
899 s = cgi.escape(s)
900 s = self.hyper_re.sub(self._hyper_repl, s)
901 return s
903 def stext(self, escape=0):
904 ''' Render the value of the property as StructuredText.
906 This requires the StructureText module to be installed separately.
907 '''
908 s = self.plain(escape=escape)
909 if not StructuredText:
910 return s
911 return StructuredText(s,level=1,header=0)
913 def field(self, size = 30):
914 ''' Render a form edit field for the property
915 '''
916 if self._value is None:
917 value = ''
918 else:
919 value = cgi.escape(str(self._value))
920 value = '"'.join(value.split('"'))
921 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
923 def multiline(self, escape=0, rows=5, cols=40):
924 ''' Render a multiline form edit field for the property
925 '''
926 if self._value is None:
927 value = ''
928 else:
929 value = cgi.escape(str(self._value))
930 value = '"'.join(value.split('"'))
931 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
932 self._formname, rows, cols, value)
934 def email(self, escape=1):
935 ''' Render the value of the property as an obscured email address
936 '''
937 if self._value is None: value = ''
938 else: value = str(self._value)
939 if value.find('@') != -1:
940 name, domain = value.split('@')
941 domain = ' '.join(domain.split('.')[:-1])
942 name = name.replace('.', ' ')
943 value = '%s at %s ...'%(name, domain)
944 else:
945 value = value.replace('.', ' ')
946 if escape:
947 value = cgi.escape(value)
948 return value
950 class PasswordHTMLProperty(HTMLProperty):
951 def plain(self):
952 ''' Render a "plain" representation of the property
953 '''
954 if self._value is None:
955 return ''
956 return _('*encrypted*')
958 def field(self, size = 30):
959 ''' Render a form edit field for the property.
960 '''
961 return '<input type="password" name="%s" size="%s">'%(self._formname, size)
963 def confirm(self, size = 30):
964 ''' Render a second form edit field for the property, used for
965 confirmation that the user typed the password correctly. Generates
966 a field with name "@confirm@name".
967 '''
968 return '<input type="password" name="@confirm@%s" size="%s">'%(
969 self._formname, size)
971 class NumberHTMLProperty(HTMLProperty):
972 def plain(self):
973 ''' Render a "plain" representation of the property
974 '''
975 return str(self._value)
977 def field(self, size = 30):
978 ''' Render a form edit field for the property
979 '''
980 if self._value is None:
981 value = ''
982 else:
983 value = cgi.escape(str(self._value))
984 value = '"'.join(value.split('"'))
985 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
987 def __int__(self):
988 ''' Return an int of me
989 '''
990 return int(self._value)
992 def __float__(self):
993 ''' Return a float of me
994 '''
995 return float(self._value)
998 class BooleanHTMLProperty(HTMLProperty):
999 def plain(self):
1000 ''' Render a "plain" representation of the property
1001 '''
1002 if self._value is None:
1003 return ''
1004 return self._value and "Yes" or "No"
1006 def field(self):
1007 ''' Render a form edit field for the property
1008 '''
1009 checked = self._value and "checked" or ""
1010 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
1011 checked)
1012 if checked:
1013 checked = ""
1014 else:
1015 checked = "checked"
1016 s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
1017 checked)
1018 return s
1020 class DateHTMLProperty(HTMLProperty):
1021 def plain(self):
1022 ''' Render a "plain" representation of the property
1023 '''
1024 if self._value is None:
1025 return ''
1026 return str(self._value.local(self._db.getUserTimezone()))
1028 def now(self):
1029 ''' Return the current time.
1031 This is useful for defaulting a new value. Returns a
1032 DateHTMLProperty.
1033 '''
1034 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1035 self._formname, date.Date('.'))
1037 def field(self, size = 30):
1038 ''' Render a form edit field for the property
1039 '''
1040 if self._value is None:
1041 value = ''
1042 else:
1043 value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1044 value = '"'.join(value.split('"'))
1045 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1047 def reldate(self, pretty=1):
1048 ''' Render the interval between the date and now.
1050 If the "pretty" flag is true, then make the display pretty.
1051 '''
1052 if not self._value:
1053 return ''
1055 # figure the interval
1056 interval = date.Date('.') - self._value
1057 if pretty:
1058 return interval.pretty()
1059 return str(interval)
1061 _marker = []
1062 def pretty(self, format=_marker):
1063 ''' Render the date in a pretty format (eg. month names, spaces).
1065 The format string is a standard python strftime format string.
1066 Note that if the day is zero, and appears at the start of the
1067 string, then it'll be stripped from the output. This is handy
1068 for the situatin when a date only specifies a month and a year.
1069 '''
1070 if format is not self._marker:
1071 return self._value.pretty(format)
1072 else:
1073 return self._value.pretty()
1075 def local(self, offset):
1076 ''' Return the date/time as a local (timezone offset) date/time.
1077 '''
1078 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1079 self._formname, self._value.local(offset))
1081 class IntervalHTMLProperty(HTMLProperty):
1082 def plain(self):
1083 ''' Render a "plain" representation of the property
1084 '''
1085 if self._value is None:
1086 return ''
1087 return str(self._value)
1089 def pretty(self):
1090 ''' Render the interval in a pretty format (eg. "yesterday")
1091 '''
1092 return self._value.pretty()
1094 def field(self, size = 30):
1095 ''' Render a form edit field for the property
1096 '''
1097 if self._value is None:
1098 value = ''
1099 else:
1100 value = cgi.escape(str(self._value))
1101 value = '"'.join(value.split('"'))
1102 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1104 class LinkHTMLProperty(HTMLProperty):
1105 ''' Link HTMLProperty
1106 Include the above as well as being able to access the class
1107 information. Stringifying the object itself results in the value
1108 from the item being displayed. Accessing attributes of this object
1109 result in the appropriate entry from the class being queried for the
1110 property accessed (so item/assignedto/name would look up the user
1111 entry identified by the assignedto property on item, and then the
1112 name property of that user)
1113 '''
1114 def __init__(self, *args, **kw):
1115 HTMLProperty.__init__(self, *args, **kw)
1116 # if we're representing a form value, then the -1 from the form really
1117 # should be a None
1118 if str(self._value) == '-1':
1119 self._value = None
1121 def __getattr__(self, attr):
1122 ''' return a new HTMLItem '''
1123 #print 'Link.getattr', (self, attr, self._value)
1124 if not self._value:
1125 raise AttributeError, "Can't access missing value"
1126 if self._prop.classname == 'user':
1127 klass = HTMLUser
1128 else:
1129 klass = HTMLItem
1130 i = klass(self._client, self._prop.classname, self._value)
1131 return getattr(i, attr)
1133 def plain(self, escape=0):
1134 ''' Render a "plain" representation of the property
1135 '''
1136 if self._value is None:
1137 return ''
1138 linkcl = self._db.classes[self._prop.classname]
1139 k = linkcl.labelprop(1)
1140 value = str(linkcl.get(self._value, k))
1141 if escape:
1142 value = cgi.escape(value)
1143 return value
1145 def field(self, showid=0, size=None):
1146 ''' Render a form edit field for the property
1147 '''
1148 linkcl = self._db.getclass(self._prop.classname)
1149 if linkcl.getprops().has_key('order'):
1150 sort_on = 'order'
1151 else:
1152 sort_on = linkcl.labelprop()
1153 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1154 # TODO: make this a field display, not a menu one!
1155 l = ['<select name="%s">'%self._formname]
1156 k = linkcl.labelprop(1)
1157 if self._value is None:
1158 s = 'selected '
1159 else:
1160 s = ''
1161 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1163 # make sure we list the current value if it's retired
1164 if self._value and self._value not in options:
1165 options.insert(0, self._value)
1167 for optionid in options:
1168 # get the option value, and if it's None use an empty string
1169 option = linkcl.get(optionid, k) or ''
1171 # figure if this option is selected
1172 s = ''
1173 if optionid == self._value:
1174 s = 'selected '
1176 # figure the label
1177 if showid:
1178 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1179 else:
1180 lab = option
1182 # truncate if it's too long
1183 if size is not None and len(lab) > size:
1184 lab = lab[:size-3] + '...'
1186 # and generate
1187 lab = cgi.escape(lab)
1188 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1189 l.append('</select>')
1190 return '\n'.join(l)
1192 def menu(self, size=None, height=None, showid=0, additional=[],
1193 **conditions):
1194 ''' Render a form select list for this property
1195 '''
1196 value = self._value
1198 linkcl = self._db.getclass(self._prop.classname)
1199 l = ['<select name="%s">'%self._formname]
1200 k = linkcl.labelprop(1)
1201 s = ''
1202 if value is None:
1203 s = 'selected '
1204 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1205 if linkcl.getprops().has_key('order'):
1206 sort_on = ('+', 'order')
1207 else:
1208 sort_on = ('+', linkcl.labelprop())
1209 options = linkcl.filter(None, conditions, sort_on, (None, None))
1211 # make sure we list the current value if it's retired
1212 if self._value and self._value not in options:
1213 options.insert(0, self._value)
1215 for optionid in options:
1216 # get the option value, and if it's None use an empty string
1217 option = linkcl.get(optionid, k) or ''
1219 # figure if this option is selected
1220 s = ''
1221 if value in [optionid, option]:
1222 s = 'selected '
1224 # figure the label
1225 if showid:
1226 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1227 else:
1228 lab = option
1230 # truncate if it's too long
1231 if size is not None and len(lab) > size:
1232 lab = lab[:size-3] + '...'
1233 if additional:
1234 m = []
1235 for propname in additional:
1236 m.append(linkcl.get(optionid, propname))
1237 lab = lab + ' (%s)'%', '.join(map(str, m))
1239 # and generate
1240 lab = cgi.escape(lab)
1241 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1242 l.append('</select>')
1243 return '\n'.join(l)
1244 # def checklist(self, ...)
1246 class MultilinkHTMLProperty(HTMLProperty):
1247 ''' Multilink HTMLProperty
1249 Also be iterable, returning a wrapper object like the Link case for
1250 each entry in the multilink.
1251 '''
1252 def __init__(self, *args, **kwargs):
1253 HTMLProperty.__init__(self, *args, **kwargs)
1254 if self._value:
1255 self._value.sort(make_sort_function(self._db, self._prop.classname))
1257 def __len__(self):
1258 ''' length of the multilink '''
1259 return len(self._value)
1261 def __getattr__(self, attr):
1262 ''' no extended attribute accesses make sense here '''
1263 raise AttributeError, attr
1265 def __getitem__(self, num):
1266 ''' iterate and return a new HTMLItem
1267 '''
1268 #print 'Multi.getitem', (self, num)
1269 value = self._value[num]
1270 if self._prop.classname == 'user':
1271 klass = HTMLUser
1272 else:
1273 klass = HTMLItem
1274 return klass(self._client, self._prop.classname, value)
1276 def __contains__(self, value):
1277 ''' Support the "in" operator. We have to make sure the passed-in
1278 value is a string first, not a *HTMLProperty.
1279 '''
1280 return str(value) in self._value
1282 def reverse(self):
1283 ''' return the list in reverse order
1284 '''
1285 l = self._value[:]
1286 l.reverse()
1287 if self._prop.classname == 'user':
1288 klass = HTMLUser
1289 else:
1290 klass = HTMLItem
1291 return [klass(self._client, self._prop.classname, value) for value in l]
1293 def plain(self, escape=0):
1294 ''' Render a "plain" representation of the property
1295 '''
1296 linkcl = self._db.classes[self._prop.classname]
1297 k = linkcl.labelprop(1)
1298 labels = []
1299 for v in self._value:
1300 labels.append(linkcl.get(v, k))
1301 value = ', '.join(labels)
1302 if escape:
1303 value = cgi.escape(value)
1304 return value
1306 def field(self, size=30, showid=0):
1307 ''' Render a form edit field for the property
1308 '''
1309 linkcl = self._db.getclass(self._prop.classname)
1310 value = self._value[:]
1311 # map the id to the label property
1312 if not linkcl.getkey():
1313 showid=1
1314 if not showid:
1315 k = linkcl.labelprop(1)
1316 value = [linkcl.get(v, k) for v in value]
1317 value = cgi.escape(','.join(value))
1318 return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1320 def menu(self, size=None, height=None, showid=0, additional=[],
1321 **conditions):
1322 ''' Render a form select list for this property
1323 '''
1324 value = self._value
1326 linkcl = self._db.getclass(self._prop.classname)
1327 sort_on = ('+', find_sort_key(linkcl))
1328 options = linkcl.filter(None, conditions, sort_on)
1329 height = height or min(len(options), 7)
1330 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1331 k = linkcl.labelprop(1)
1333 # make sure we list the current values if they're retired
1334 for val in value:
1335 if val not in options:
1336 options.insert(0, val)
1338 for optionid in options:
1339 # get the option value, and if it's None use an empty string
1340 option = linkcl.get(optionid, k) or ''
1342 # figure if this option is selected
1343 s = ''
1344 if optionid in value or option in value:
1345 s = 'selected '
1347 # figure the label
1348 if showid:
1349 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1350 else:
1351 lab = option
1352 # truncate if it's too long
1353 if size is not None and len(lab) > size:
1354 lab = lab[:size-3] + '...'
1355 if additional:
1356 m = []
1357 for propname in additional:
1358 m.append(linkcl.get(optionid, propname))
1359 lab = lab + ' (%s)'%', '.join(m)
1361 # and generate
1362 lab = cgi.escape(lab)
1363 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1364 lab))
1365 l.append('</select>')
1366 return '\n'.join(l)
1368 # set the propclasses for HTMLItem
1369 propclasses = (
1370 (hyperdb.String, StringHTMLProperty),
1371 (hyperdb.Number, NumberHTMLProperty),
1372 (hyperdb.Boolean, BooleanHTMLProperty),
1373 (hyperdb.Date, DateHTMLProperty),
1374 (hyperdb.Interval, IntervalHTMLProperty),
1375 (hyperdb.Password, PasswordHTMLProperty),
1376 (hyperdb.Link, LinkHTMLProperty),
1377 (hyperdb.Multilink, MultilinkHTMLProperty),
1378 )
1380 def make_sort_function(db, classname):
1381 '''Make a sort function for a given class
1382 '''
1383 linkcl = db.getclass(classname)
1384 sort_on = find_sort_key(linkcl)
1385 def sortfunc(a, b):
1386 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1387 return sortfunc
1389 def find_sort_key(linkcl):
1390 if linkcl.getprops().has_key('order'):
1391 return 'order'
1392 else:
1393 return linkcl.labelprop()
1395 def handleListCGIValue(value):
1396 ''' Value is either a single item or a list of items. Each item has a
1397 .value that we're actually interested in.
1398 '''
1399 if isinstance(value, type([])):
1400 return [value.value for value in value]
1401 else:
1402 value = value.value.strip()
1403 if not value:
1404 return []
1405 return value.split(',')
1407 class ShowDict:
1408 ''' A convenience access to the :columns index parameters
1409 '''
1410 def __init__(self, columns):
1411 self.columns = {}
1412 for col in columns:
1413 self.columns[col] = 1
1414 def __getitem__(self, name):
1415 return self.columns.has_key(name)
1417 class HTMLRequest:
1418 ''' The *request*, holding the CGI form and environment.
1420 "form" the CGI form as a cgi.FieldStorage
1421 "env" the CGI environment variables
1422 "base" the base URL for this instance
1423 "user" a HTMLUser instance for this user
1424 "classname" the current classname (possibly None)
1425 "template" the current template (suffix, also possibly None)
1427 Index args:
1428 "columns" dictionary of the columns to display in an index page
1429 "show" a convenience access to columns - request/show/colname will
1430 be true if the columns should be displayed, false otherwise
1431 "sort" index sort column (direction, column name)
1432 "group" index grouping property (direction, column name)
1433 "filter" properties to filter the index on
1434 "filterspec" values to filter the index on
1435 "search_text" text to perform a full-text search on for an index
1437 '''
1438 def __init__(self, client):
1439 self.client = client
1441 # easier access vars
1442 self.form = client.form
1443 self.env = client.env
1444 self.base = client.base
1445 self.user = HTMLUser(client, 'user', client.userid)
1447 # store the current class name and action
1448 self.classname = client.classname
1449 self.template = client.template
1451 # the special char to use for special vars
1452 self.special_char = '@'
1454 self._post_init()
1456 def _post_init(self):
1457 ''' Set attributes based on self.form
1458 '''
1459 # extract the index display information from the form
1460 self.columns = []
1461 for name in ':columns @columns'.split():
1462 if self.form.has_key(name):
1463 self.special_char = name[0]
1464 self.columns = handleListCGIValue(self.form[name])
1465 break
1466 self.show = ShowDict(self.columns)
1468 # sorting
1469 self.sort = (None, None)
1470 for name in ':sort @sort'.split():
1471 if self.form.has_key(name):
1472 self.special_char = name[0]
1473 sort = self.form[name].value
1474 if sort.startswith('-'):
1475 self.sort = ('-', sort[1:])
1476 else:
1477 self.sort = ('+', sort)
1478 if self.form.has_key(self.special_char+'sortdir'):
1479 self.sort = ('-', self.sort[1])
1481 # grouping
1482 self.group = (None, None)
1483 for name in ':group @group'.split():
1484 if self.form.has_key(name):
1485 self.special_char = name[0]
1486 group = self.form[name].value
1487 if group.startswith('-'):
1488 self.group = ('-', group[1:])
1489 else:
1490 self.group = ('+', group)
1491 if self.form.has_key(self.special_char+'groupdir'):
1492 self.group = ('-', self.group[1])
1494 # filtering
1495 self.filter = []
1496 for name in ':filter @filter'.split():
1497 if self.form.has_key(name):
1498 self.special_char = name[0]
1499 self.filter = handleListCGIValue(self.form[name])
1501 self.filterspec = {}
1502 db = self.client.db
1503 if self.classname is not None:
1504 props = db.getclass(self.classname).getprops()
1505 for name in self.filter:
1506 if not self.form.has_key(name):
1507 continue
1508 prop = props[name]
1509 fv = self.form[name]
1510 if (isinstance(prop, hyperdb.Link) or
1511 isinstance(prop, hyperdb.Multilink)):
1512 self.filterspec[name] = lookupIds(db, prop,
1513 handleListCGIValue(fv))
1514 else:
1515 if isinstance(fv, type([])):
1516 self.filterspec[name] = [v.value for v in fv]
1517 else:
1518 self.filterspec[name] = fv.value
1520 # full-text search argument
1521 self.search_text = None
1522 for name in ':search_text @search_text'.split():
1523 if self.form.has_key(name):
1524 self.special_char = name[0]
1525 self.search_text = self.form[name].value
1527 # pagination - size and start index
1528 # figure batch args
1529 self.pagesize = 50
1530 for name in ':pagesize @pagesize'.split():
1531 if self.form.has_key(name):
1532 self.special_char = name[0]
1533 self.pagesize = int(self.form[name].value)
1535 self.startwith = 0
1536 for name in ':startwith @startwith'.split():
1537 if self.form.has_key(name):
1538 self.special_char = name[0]
1539 self.startwith = int(self.form[name].value)
1541 def updateFromURL(self, url):
1542 ''' Parse the URL for query args, and update my attributes using the
1543 values.
1544 '''
1545 env = {'QUERY_STRING': url}
1546 self.form = cgi.FieldStorage(environ=env)
1548 self._post_init()
1550 def update(self, kwargs):
1551 ''' Update my attributes using the keyword args
1552 '''
1553 self.__dict__.update(kwargs)
1554 if kwargs.has_key('columns'):
1555 self.show = ShowDict(self.columns)
1557 def description(self):
1558 ''' Return a description of the request - handle for the page title.
1559 '''
1560 s = [self.client.db.config.TRACKER_NAME]
1561 if self.classname:
1562 if self.client.nodeid:
1563 s.append('- %s%s'%(self.classname, self.client.nodeid))
1564 else:
1565 if self.template == 'item':
1566 s.append('- new %s'%self.classname)
1567 elif self.template == 'index':
1568 s.append('- %s index'%self.classname)
1569 else:
1570 s.append('- %s %s'%(self.classname, self.template))
1571 else:
1572 s.append('- home')
1573 return ' '.join(s)
1575 def __str__(self):
1576 d = {}
1577 d.update(self.__dict__)
1578 f = ''
1579 for k in self.form.keys():
1580 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1581 d['form'] = f
1582 e = ''
1583 for k,v in self.env.items():
1584 e += '\n %r=%r'%(k, v)
1585 d['env'] = e
1586 return '''
1587 form: %(form)s
1588 base: %(base)r
1589 classname: %(classname)r
1590 template: %(template)r
1591 columns: %(columns)r
1592 sort: %(sort)r
1593 group: %(group)r
1594 filter: %(filter)r
1595 search_text: %(search_text)r
1596 pagesize: %(pagesize)r
1597 startwith: %(startwith)r
1598 env: %(env)s
1599 '''%d
1601 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1602 filterspec=1):
1603 ''' return the current index args as form elements '''
1604 l = []
1605 sc = self.special_char
1606 s = '<input type="hidden" name="%s" value="%s">'
1607 if columns and self.columns:
1608 l.append(s%(sc+'columns', ','.join(self.columns)))
1609 if sort and self.sort[1] is not None:
1610 if self.sort[0] == '-':
1611 val = '-'+self.sort[1]
1612 else:
1613 val = self.sort[1]
1614 l.append(s%(sc+'sort', val))
1615 if group and self.group[1] is not None:
1616 if self.group[0] == '-':
1617 val = '-'+self.group[1]
1618 else:
1619 val = self.group[1]
1620 l.append(s%(sc+'group', val))
1621 if filter and self.filter:
1622 l.append(s%(sc+'filter', ','.join(self.filter)))
1623 if filterspec:
1624 for k,v in self.filterspec.items():
1625 if type(v) == type([]):
1626 l.append(s%(k, ','.join(v)))
1627 else:
1628 l.append(s%(k, v))
1629 if self.search_text:
1630 l.append(s%(sc+'search_text', self.search_text))
1631 l.append(s%(sc+'pagesize', self.pagesize))
1632 l.append(s%(sc+'startwith', self.startwith))
1633 return '\n'.join(l)
1635 def indexargs_url(self, url, args):
1636 ''' Embed the current index args in a URL
1637 '''
1638 sc = self.special_char
1639 l = ['%s=%s'%(k,v) for k,v in args.items()]
1641 # pull out the special values (prefixed by @ or :)
1642 specials = {}
1643 for key in args.keys():
1644 if key[0] in '@:':
1645 specials[key[1:]] = args[key]
1647 # ok, now handle the specials we received in the request
1648 if self.columns and not specials.has_key('columns'):
1649 l.append(sc+'columns=%s'%(','.join(self.columns)))
1650 if self.sort[1] is not None and not specials.has_key('sort'):
1651 if self.sort[0] == '-':
1652 val = '-'+self.sort[1]
1653 else:
1654 val = self.sort[1]
1655 l.append(sc+'sort=%s'%val)
1656 if self.group[1] is not None and not specials.has_key('group'):
1657 if self.group[0] == '-':
1658 val = '-'+self.group[1]
1659 else:
1660 val = self.group[1]
1661 l.append(sc+'group=%s'%val)
1662 if self.filter and not specials.has_key('filter'):
1663 l.append(sc+'filter=%s'%(','.join(self.filter)))
1664 if self.search_text and not specials.has_key('search_text'):
1665 l.append(sc+'search_text=%s'%self.search_text)
1666 if not specials.has_key('pagesize'):
1667 l.append(sc+'pagesize=%s'%self.pagesize)
1668 if not specials.has_key('startwith'):
1669 l.append(sc+'startwith=%s'%self.startwith)
1671 # finally, the remainder of the filter args in the request
1672 for k,v in self.filterspec.items():
1673 if not args.has_key(k):
1674 if type(v) == type([]):
1675 l.append('%s=%s'%(k, ','.join(v)))
1676 else:
1677 l.append('%s=%s'%(k, v))
1678 return '%s?%s'%(url, '&'.join(l))
1679 indexargs_href = indexargs_url
1681 def base_javascript(self):
1682 return '''
1683 <script type="text/javascript">
1684 submitted = false;
1685 function submit_once() {
1686 if (submitted) {
1687 alert("Your request is being processed.\\nPlease be patient.");
1688 return 0;
1689 }
1690 submitted = true;
1691 return 1;
1692 }
1694 function help_window(helpurl, width, height) {
1695 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1696 }
1697 </script>
1698 '''%self.base
1700 def batch(self):
1701 ''' Return a batch object for results from the "current search"
1702 '''
1703 filterspec = self.filterspec
1704 sort = self.sort
1705 group = self.group
1707 # get the list of ids we're batching over
1708 klass = self.client.db.getclass(self.classname)
1709 if self.search_text:
1710 matches = self.client.db.indexer.search(
1711 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1712 else:
1713 matches = None
1714 l = klass.filter(matches, filterspec, sort, group)
1716 # return the batch object, using IDs only
1717 return Batch(self.client, l, self.pagesize, self.startwith,
1718 classname=self.classname)
1720 # extend the standard ZTUtils Batch object to remove dependency on
1721 # Acquisition and add a couple of useful methods
1722 class Batch(ZTUtils.Batch):
1723 ''' Use me to turn a list of items, or item ids of a given class, into a
1724 series of batches.
1726 ========= ========================================================
1727 Parameter Usage
1728 ========= ========================================================
1729 sequence a list of HTMLItems or item ids
1730 classname if sequence is a list of ids, this is the class of item
1731 size how big to make the sequence.
1732 start where to start (0-indexed) in the sequence.
1733 end where to end (0-indexed) in the sequence.
1734 orphan if the next batch would contain less items than this
1735 value, then it is combined with this batch
1736 overlap the number of items shared between adjacent batches
1737 ========= ========================================================
1739 Attributes: Note that the "start" attribute, unlike the
1740 argument, is a 1-based index (I know, lame). "first" is the
1741 0-based index. "length" is the actual number of elements in
1742 the batch.
1744 "sequence_length" is the length of the original, unbatched, sequence.
1745 '''
1746 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1747 overlap=0, classname=None):
1748 self.client = client
1749 self.last_index = self.last_item = None
1750 self.current_item = None
1751 self.classname = classname
1752 self.sequence_length = len(sequence)
1753 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1754 overlap)
1756 # overwrite so we can late-instantiate the HTMLItem instance
1757 def __getitem__(self, index):
1758 if index < 0:
1759 if index + self.end < self.first: raise IndexError, index
1760 return self._sequence[index + self.end]
1762 if index >= self.length:
1763 raise IndexError, index
1765 # move the last_item along - but only if the fetched index changes
1766 # (for some reason, index 0 is fetched twice)
1767 if index != self.last_index:
1768 self.last_item = self.current_item
1769 self.last_index = index
1771 item = self._sequence[index + self.first]
1772 if self.classname:
1773 # map the item ids to instances
1774 if self.classname == 'user':
1775 item = HTMLUser(self.client, self.classname, item)
1776 else:
1777 item = HTMLItem(self.client, self.classname, item)
1778 self.current_item = item
1779 return item
1781 def propchanged(self, property):
1782 ''' Detect if the property marked as being the group property
1783 changed in the last iteration fetch
1784 '''
1785 if (self.last_item is None or
1786 self.last_item[property] != self.current_item[property]):
1787 return 1
1788 return 0
1790 # override these 'cos we don't have access to acquisition
1791 def previous(self):
1792 if self.start == 1:
1793 return None
1794 return Batch(self.client, self._sequence, self._size,
1795 self.first - self._size + self.overlap, 0, self.orphan,
1796 self.overlap)
1798 def next(self):
1799 try:
1800 self._sequence[self.end]
1801 except IndexError:
1802 return None
1803 return Batch(self.client, self._sequence, self._size,
1804 self.end - self.overlap, 0, self.orphan, self.overlap)
1806 class TemplatingUtils:
1807 ''' Utilities for templating
1808 '''
1809 def __init__(self, client):
1810 self.client = client
1811 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1812 return Batch(self.client, sequence, size, start, end, orphan,
1813 overlap)