1 import sys, cgi, urllib, os, re, os.path, time, errno
3 from roundup import hyperdb, date
4 from roundup.i18n import _
6 try:
7 import cPickle as pickle
8 except ImportError:
9 import pickle
10 try:
11 import cStringIO as StringIO
12 except ImportError:
13 import StringIO
14 try:
15 import StructuredText
16 except ImportError:
17 StructuredText = None
19 # bring in the templating support
20 from roundup.cgi.PageTemplates import PageTemplate
21 from roundup.cgi.PageTemplates.Expressions import getEngine
22 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
23 from roundup.cgi import ZTUtils
25 class NoTemplate(Exception):
26 pass
28 class Templates:
29 templates = {}
31 def __init__(self, dir):
32 self.dir = dir
34 def precompileTemplates(self):
35 ''' Go through a directory and precompile all the templates therein
36 '''
37 for filename in os.listdir(self.dir):
38 if os.path.isdir(filename): continue
39 if '.' in filename:
40 name, extension = filename.split('.')
41 self.getTemplate(name, extension)
42 else:
43 self.getTemplate(filename, None)
45 def get(self, name, extension):
46 ''' Interface to get a template, possibly loading a compiled template.
48 "name" and "extension" indicate the template we're after, which in
49 most cases will be "name.extension". If "extension" is None, then
50 we look for a template just called "name" with no extension.
52 If the file "name.extension" doesn't exist, we look for
53 "_generic.extension" as a fallback.
54 '''
55 # default the name to "home"
56 if name is None:
57 name = 'home'
59 # find the source, figure the time it was last modified
60 if extension:
61 filename = '%s.%s'%(name, extension)
62 else:
63 filename = name
64 src = os.path.join(self.dir, filename)
65 try:
66 stime = os.stat(src)[os.path.stat.ST_MTIME]
67 except os.error, error:
68 if error.errno != errno.ENOENT:
69 raise
70 if not extension:
71 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
73 # try for a generic template
74 generic = '_generic.%s'%extension
75 src = os.path.join(self.dir, generic)
76 try:
77 stime = os.stat(src)[os.path.stat.ST_MTIME]
78 except os.error, error:
79 if error.errno != errno.ENOENT:
80 raise
81 # nicer error
82 raise NoTemplate, 'No template file exists for templating '\
83 '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
84 extension, filename, generic)
85 filename = generic
87 if self.templates.has_key(filename) and \
88 stime < self.templates[filename].mtime:
89 # compiled template is up to date
90 return self.templates[filename]
92 # compile the template
93 self.templates[filename] = pt = RoundupPageTemplate()
94 pt.write(open(src).read())
95 pt.id = filename
96 pt.mtime = time.time()
97 return pt
99 def __getitem__(self, name):
100 name, extension = os.path.splitext(name)
101 if extension:
102 extension = extension[1:]
103 try:
104 return self.get(name, extension)
105 except NoTemplate, message:
106 raise KeyError, message
108 class RoundupPageTemplate(PageTemplate.PageTemplate):
109 ''' A Roundup-specific PageTemplate.
111 Interrogate the client to set up the various template variables to
112 be available:
114 *context*
115 this is one of three things:
116 1. None - we're viewing a "home" page
117 2. The current class of item being displayed. This is an HTMLClass
118 instance.
119 3. The current item from the database, if we're viewing a specific
120 item, as an HTMLItem instance.
121 *request*
122 Includes information about the current request, including:
123 - the url
124 - the current index information (``filterspec``, ``filter`` args,
125 ``properties``, etc) parsed out of the form.
126 - methods for easy filterspec link generation
127 - *user*, the current user node as an HTMLItem instance
128 - *form*, the current CGI form information as a FieldStorage
129 *tracker*
130 The current tracker
131 *db*
132 The current database, through which db.config may be reached.
133 '''
134 def getContext(self, client, classname, request):
135 c = {
136 'options': {},
137 'nothing': None,
138 'request': request,
139 'db': HTMLDatabase(client),
140 'tracker': client.instance,
141 'utils': TemplatingUtils(client),
142 'templates': Templates(client.instance.config.TEMPLATES),
143 }
144 # add in the item if there is one
145 if client.nodeid:
146 if classname == 'user':
147 c['context'] = HTMLUser(client, classname, client.nodeid)
148 else:
149 c['context'] = HTMLItem(client, classname, client.nodeid)
150 elif client.db.classes.has_key(classname):
151 c['context'] = HTMLClass(client, classname)
152 return c
154 def render(self, client, classname, request, **options):
155 """Render this Page Template"""
157 if not self._v_cooked:
158 self._cook()
160 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
162 if self._v_errors:
163 raise PageTemplate.PTRuntimeError, \
164 'Page Template %s has errors.'%self.id
166 # figure the context
167 classname = classname or client.classname
168 request = request or HTMLRequest(client)
169 c = self.getContext(client, classname, request)
170 c.update({'options': options})
172 # and go
173 output = StringIO.StringIO()
174 TALInterpreter(self._v_program, self.macros,
175 getEngine().getContext(c), output, tal=1, strictinsert=0)()
176 return output.getvalue()
178 class HTMLDatabase:
179 ''' Return HTMLClasses for valid class fetches
180 '''
181 def __init__(self, client):
182 self._client = client
184 # we want config to be exposed
185 self.config = client.db.config
187 def __getitem__(self, item):
188 self._client.db.getclass(item)
189 return HTMLClass(self._client, item)
191 def __getattr__(self, attr):
192 try:
193 return self[attr]
194 except KeyError:
195 raise AttributeError, attr
197 def classes(self):
198 l = self._client.db.classes.keys()
199 l.sort()
200 return [HTMLClass(self._client, cn) for cn in l]
202 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
203 cl = db.getclass(prop.classname)
204 l = []
205 for entry in ids:
206 if num_re.match(entry):
207 l.append(entry)
208 else:
209 try:
210 l.append(cl.lookup(entry))
211 except KeyError:
212 # ignore invalid keys
213 pass
214 return l
216 class HTMLPermissions:
217 ''' Helpers that provide answers to commonly asked Permission questions.
218 '''
219 def is_edit_ok(self):
220 ''' Is the user allowed to Edit the current class?
221 '''
222 return self._db.security.hasPermission('Edit', self._client.userid,
223 self._classname)
224 def is_view_ok(self):
225 ''' Is the user allowed to View the current class?
226 '''
227 return self._db.security.hasPermission('View', self._client.userid,
228 self._classname)
229 def is_only_view_ok(self):
230 ''' Is the user only allowed to View (ie. not Edit) the current class?
231 '''
232 return self.is_view_ok() and not self.is_edit_ok()
234 class HTMLClass(HTMLPermissions):
235 ''' Accesses through a class (either through *class* or *db.<classname>*)
236 '''
237 def __init__(self, client, classname):
238 self._client = client
239 self._db = client.db
241 # we want classname to be exposed, but _classname gives a
242 # consistent API for extending Class/Item
243 self._classname = self.classname = classname
244 self._klass = self._db.getclass(self.classname)
245 self._props = self._klass.getprops()
247 def __repr__(self):
248 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
250 def __getitem__(self, item):
251 ''' return an HTMLProperty instance
252 '''
253 #print 'HTMLClass.getitem', (self, item)
255 # we don't exist
256 if item == 'id':
257 return None
259 # get the property
260 prop = self._props[item]
262 # look up the correct HTMLProperty class
263 form = self._client.form
264 for klass, htmlklass in propclasses:
265 if not isinstance(prop, klass):
266 continue
267 if form.has_key(item):
268 if isinstance(prop, hyperdb.Multilink):
269 value = lookupIds(self._db, prop,
270 handleListCGIValue(form[item]))
271 elif isinstance(prop, hyperdb.Link):
272 value = form[item].value.strip()
273 if value:
274 value = lookupIds(self._db, prop, [value])[0]
275 else:
276 value = None
277 else:
278 value = form[item].value.strip() or None
279 else:
280 if isinstance(prop, hyperdb.Multilink):
281 value = []
282 else:
283 value = None
284 return htmlklass(self._client, '', prop, item, value)
286 # no good
287 raise KeyError, item
289 def __getattr__(self, attr):
290 ''' convenience access '''
291 try:
292 return self[attr]
293 except KeyError:
294 raise AttributeError, attr
296 def getItem(self, itemid, num_re=re.compile('\d+')):
297 ''' Get an item of this class by its item id.
298 '''
299 # make sure we're looking at an itemid
300 if not num_re.match(itemid):
301 itemid = self._klass.lookup(itemid)
303 if self.classname == 'user':
304 klass = HTMLUser
305 else:
306 klass = HTMLItem
308 return klass(self._client, self.classname, itemid)
310 def properties(self):
311 ''' Return HTMLProperty for all of this class' properties.
312 '''
313 l = []
314 for name, prop in self._props.items():
315 for klass, htmlklass in propclasses:
316 if isinstance(prop, hyperdb.Multilink):
317 value = []
318 else:
319 value = None
320 if isinstance(prop, klass):
321 l.append(htmlklass(self._client, '', prop, name, value))
322 return l
324 def list(self):
325 ''' List all items in this class.
326 '''
327 if self.classname == 'user':
328 klass = HTMLUser
329 else:
330 klass = HTMLItem
332 # get the list and sort it nicely
333 l = self._klass.list()
334 sortfunc = make_sort_function(self._db, self.classname)
335 l.sort(sortfunc)
337 l = [klass(self._client, self.classname, x) for x in l]
338 return l
340 def csv(self):
341 ''' Return the items of this class as a chunk of CSV text.
342 '''
343 # get the CSV module
344 try:
345 import csv
346 except ImportError:
347 return 'Sorry, you need the csv module to use this function.\n'\
348 'Get it from: http://www.object-craft.com.au/projects/csv/'
350 props = self.propnames()
351 p = csv.parser()
352 s = StringIO.StringIO()
353 s.write(p.join(props) + '\n')
354 for nodeid in self._klass.list():
355 l = []
356 for name in props:
357 value = self._klass.get(nodeid, name)
358 if value is None:
359 l.append('')
360 elif isinstance(value, type([])):
361 l.append(':'.join(map(str, value)))
362 else:
363 l.append(str(self._klass.get(nodeid, name)))
364 s.write(p.join(l) + '\n')
365 return s.getvalue()
367 def propnames(self):
368 ''' Return the list of the names of the properties of this class.
369 '''
370 idlessprops = self._klass.getprops(protected=0).keys()
371 idlessprops.sort()
372 return ['id'] + idlessprops
374 def filter(self, request=None):
375 ''' Return a list of items from this class, filtered and sorted
376 by the current requested filterspec/filter/sort/group args
377 '''
378 if request is not None:
379 filterspec = request.filterspec
380 sort = request.sort
381 group = request.group
382 if self.classname == 'user':
383 klass = HTMLUser
384 else:
385 klass = HTMLItem
386 l = [klass(self._client, self.classname, x)
387 for x in self._klass.filter(None, filterspec, sort, group)]
388 return l
390 def classhelp(self, properties=None, label='list', width='500',
391 height='400'):
392 ''' Pop up a javascript window with class help
394 This generates a link to a popup window which displays the
395 properties indicated by "properties" of the class named by
396 "classname". The "properties" should be a comma-separated list
397 (eg. 'id,name,description'). Properties defaults to all the
398 properties of a class (excluding id, creator, created and
399 activity).
401 You may optionally override the label displayed, the width and
402 height. The popup window will be resizable and scrollable.
403 '''
404 if properties is None:
405 properties = self._klass.getprops(protected=0).keys()
406 properties.sort()
407 properties = ','.join(properties)
408 return '<a href="javascript:help_window(\'%s?:template=help&' \
409 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
410 self.classname, properties, width, height, label)
412 def submit(self, label="Submit New Entry"):
413 ''' Generate a submit button (and action hidden element)
414 '''
415 return ' <input type="hidden" name=":action" value="new">\n'\
416 ' <input type="submit" name="submit" value="%s">'%label
418 def history(self):
419 return 'New node - no history'
421 def renderWith(self, name, **kwargs):
422 ''' Render this class with the given template.
423 '''
424 # create a new request and override the specified args
425 req = HTMLRequest(self._client)
426 req.classname = self.classname
427 req.update(kwargs)
429 # new template, using the specified classname and request
430 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
432 # use our fabricated request
433 return pt.render(self._client, self.classname, req)
435 class HTMLItem(HTMLPermissions):
436 ''' Accesses through an *item*
437 '''
438 def __init__(self, client, classname, nodeid):
439 self._client = client
440 self._db = client.db
441 self._classname = classname
442 self._nodeid = nodeid
443 self._klass = self._db.getclass(classname)
444 self._props = self._klass.getprops()
446 def __repr__(self):
447 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
448 self._nodeid)
450 def __getitem__(self, item):
451 ''' return an HTMLProperty instance
452 '''
453 #print 'HTMLItem.getitem', (self, item)
454 if item == 'id':
455 return self._nodeid
457 # get the property
458 prop = self._props[item]
460 # get the value, handling missing values
461 value = self._klass.get(self._nodeid, item, None)
462 if value is None:
463 if isinstance(self._props[item], hyperdb.Multilink):
464 value = []
466 # look up the correct HTMLProperty class
467 for klass, htmlklass in propclasses:
468 if isinstance(prop, klass):
469 return htmlklass(self._client, self._nodeid, prop, item, value)
471 raise KeyErorr, item
473 def __getattr__(self, attr):
474 ''' convenience access to properties '''
475 try:
476 return self[attr]
477 except KeyError:
478 raise AttributeError, attr
480 def submit(self, label="Submit Changes"):
481 ''' Generate a submit button (and action hidden element)
482 '''
483 return ' <input type="hidden" name=":action" value="edit">\n'\
484 ' <input type="submit" name="submit" value="%s">'%label
486 def journal(self, direction='descending'):
487 ''' Return a list of HTMLJournalEntry instances.
488 '''
489 # XXX do this
490 return []
492 def history(self, direction='descending'):
493 l = ['<table class="history">'
494 '<tr><th colspan="4" class="header">',
495 _('History'),
496 '</th></tr><tr>',
497 _('<th>Date</th>'),
498 _('<th>User</th>'),
499 _('<th>Action</th>'),
500 _('<th>Args</th>'),
501 '</tr>']
502 comments = {}
503 history = self._klass.history(self._nodeid)
504 history.sort()
505 if direction == 'descending':
506 history.reverse()
507 for id, evt_date, user, action, args in history:
508 date_s = str(evt_date).replace("."," ")
509 arg_s = ''
510 if action == 'link' and type(args) == type(()):
511 if len(args) == 3:
512 linkcl, linkid, key = args
513 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
514 linkcl, linkid, key)
515 else:
516 arg_s = str(args)
518 elif action == 'unlink' and type(args) == type(()):
519 if len(args) == 3:
520 linkcl, linkid, key = args
521 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
522 linkcl, linkid, key)
523 else:
524 arg_s = str(args)
526 elif type(args) == type({}):
527 cell = []
528 for k in args.keys():
529 # try to get the relevant property and treat it
530 # specially
531 try:
532 prop = self._props[k]
533 except KeyError:
534 prop = None
535 if prop is not None:
536 if args[k] and (isinstance(prop, hyperdb.Multilink) or
537 isinstance(prop, hyperdb.Link)):
538 # figure what the link class is
539 classname = prop.classname
540 try:
541 linkcl = self._db.getclass(classname)
542 except KeyError:
543 labelprop = None
544 comments[classname] = _('''The linked class
545 %(classname)s no longer exists''')%locals()
546 labelprop = linkcl.labelprop(1)
547 hrefable = os.path.exists(
548 os.path.join(self._db.config.TEMPLATES,
549 classname+'.item'))
551 if isinstance(prop, hyperdb.Multilink) and \
552 len(args[k]) > 0:
553 ml = []
554 for linkid in args[k]:
555 if isinstance(linkid, type(())):
556 sublabel = linkid[0] + ' '
557 linkids = linkid[1]
558 else:
559 sublabel = ''
560 linkids = [linkid]
561 subml = []
562 for linkid in linkids:
563 label = classname + linkid
564 # if we have a label property, try to use it
565 # TODO: test for node existence even when
566 # there's no labelprop!
567 try:
568 if labelprop is not None:
569 label = linkcl.get(linkid, labelprop)
570 except IndexError:
571 comments['no_link'] = _('''<strike>The
572 linked node no longer
573 exists</strike>''')
574 subml.append('<strike>%s</strike>'%label)
575 else:
576 if hrefable:
577 subml.append('<a href="%s%s">%s</a>'%(
578 classname, linkid, label))
579 ml.append(sublabel + ', '.join(subml))
580 cell.append('%s:\n %s'%(k, ', '.join(ml)))
581 elif isinstance(prop, hyperdb.Link) and args[k]:
582 label = classname + args[k]
583 # if we have a label property, try to use it
584 # TODO: test for node existence even when
585 # there's no labelprop!
586 if labelprop is not None:
587 try:
588 label = linkcl.get(args[k], labelprop)
589 except IndexError:
590 comments['no_link'] = _('''<strike>The
591 linked node no longer
592 exists</strike>''')
593 cell.append(' <strike>%s</strike>,\n'%label)
594 # "flag" this is done .... euwww
595 label = None
596 if label is not None:
597 if hrefable:
598 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
599 classname, args[k], label))
600 else:
601 cell.append('%s: %s' % (k,label))
603 elif isinstance(prop, hyperdb.Date) and args[k]:
604 d = date.Date(args[k])
605 cell.append('%s: %s'%(k, str(d)))
607 elif isinstance(prop, hyperdb.Interval) and args[k]:
608 d = date.Interval(args[k])
609 cell.append('%s: %s'%(k, str(d)))
611 elif isinstance(prop, hyperdb.String) and args[k]:
612 cell.append('%s: %s'%(k, cgi.escape(args[k])))
614 elif not args[k]:
615 cell.append('%s: (no value)\n'%k)
617 else:
618 cell.append('%s: %s\n'%(k, str(args[k])))
619 else:
620 # property no longer exists
621 comments['no_exist'] = _('''<em>The indicated property
622 no longer exists</em>''')
623 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
624 arg_s = '<br />'.join(cell)
625 else:
626 # unkown event!!
627 comments['unknown'] = _('''<strong><em>This event is not
628 handled by the history display!</em></strong>''')
629 arg_s = '<strong><em>' + str(args) + '</em></strong>'
630 date_s = date_s.replace(' ', ' ')
631 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
632 date_s, user, action, arg_s))
633 if comments:
634 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
635 for entry in comments.values():
636 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
637 l.append('</table>')
638 return '\n'.join(l)
640 def renderQueryForm(self):
641 ''' Render this item, which is a query, as a search form.
642 '''
643 # create a new request and override the specified args
644 req = HTMLRequest(self._client)
645 req.classname = self._klass.get(self._nodeid, 'klass')
646 req.updateFromURL(self._klass.get(self._nodeid, 'url'))
648 # new template, using the specified classname and request
649 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
651 # use our fabricated request
652 return pt.render(self._client, req.classname, req)
654 class HTMLUser(HTMLItem):
655 ''' Accesses through the *user* (a special case of item)
656 '''
657 def __init__(self, client, classname, nodeid):
658 HTMLItem.__init__(self, client, 'user', nodeid)
659 self._default_classname = client.classname
661 # used for security checks
662 self._security = client.db.security
664 _marker = []
665 def hasPermission(self, role, classname=_marker):
666 ''' Determine if the user has the Role.
668 The class being tested defaults to the template's class, but may
669 be overidden for this test by suppling an alternate classname.
670 '''
671 if classname is self._marker:
672 classname = self._default_classname
673 return self._security.hasPermission(role, self._nodeid, classname)
675 def is_edit_ok(self):
676 ''' Is the user allowed to Edit the current class?
677 Also check whether this is the current user's info.
678 '''
679 return self._db.security.hasPermission('Edit', self._client.userid,
680 self._classname) or self._nodeid == self._client.userid
682 def is_view_ok(self):
683 ''' Is the user allowed to View the current class?
684 Also check whether this is the current user's info.
685 '''
686 return self._db.security.hasPermission('Edit', self._client.userid,
687 self._classname) or self._nodeid == self._client.userid
689 class HTMLProperty:
690 ''' String, Number, Date, Interval HTMLProperty
692 Has useful attributes:
694 _name the name of the property
695 _value the value of the property if any
697 A wrapper object which may be stringified for the plain() behaviour.
698 '''
699 def __init__(self, client, nodeid, prop, name, value):
700 self._client = client
701 self._db = client.db
702 self._nodeid = nodeid
703 self._prop = prop
704 self._name = name
705 self._value = value
706 def __repr__(self):
707 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
708 def __str__(self):
709 return self.plain()
710 def __cmp__(self, other):
711 if isinstance(other, HTMLProperty):
712 return cmp(self._value, other._value)
713 return cmp(self._value, other)
715 class StringHTMLProperty(HTMLProperty):
716 def plain(self, escape=0):
717 ''' Render a "plain" representation of the property
718 '''
719 if self._value is None:
720 return ''
721 if escape:
722 return cgi.escape(str(self._value))
723 return str(self._value)
725 def stext(self, escape=0):
726 ''' Render the value of the property as StructuredText.
728 This requires the StructureText module to be installed separately.
729 '''
730 s = self.plain(escape=escape)
731 if not StructuredText:
732 return s
733 return StructuredText(s,level=1,header=0)
735 def field(self, size = 30):
736 ''' Render a form edit field for the property
737 '''
738 if self._value is None:
739 value = ''
740 else:
741 value = cgi.escape(str(self._value))
742 value = '"'.join(value.split('"'))
743 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
745 def multiline(self, escape=0, rows=5, cols=40):
746 ''' Render a multiline form edit field for the property
747 '''
748 if self._value is None:
749 value = ''
750 else:
751 value = cgi.escape(str(self._value))
752 value = '"'.join(value.split('"'))
753 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
754 self._name, rows, cols, value)
756 def email(self, escape=1):
757 ''' Render the value of the property as an obscured email address
758 '''
759 if self._value is None: value = ''
760 else: value = str(self._value)
761 if value.find('@') != -1:
762 name, domain = value.split('@')
763 domain = ' '.join(domain.split('.')[:-1])
764 name = name.replace('.', ' ')
765 value = '%s at %s ...'%(name, domain)
766 else:
767 value = value.replace('.', ' ')
768 if escape:
769 value = cgi.escape(value)
770 return value
772 class PasswordHTMLProperty(HTMLProperty):
773 def plain(self):
774 ''' Render a "plain" representation of the property
775 '''
776 if self._value is None:
777 return ''
778 return _('*encrypted*')
780 def field(self, size = 30):
781 ''' Render a form edit field for the property.
782 '''
783 return '<input type="password" name="%s" size="%s">'%(self._name, size)
785 def confirm(self, size = 30):
786 ''' Render a second form edit field for the property, used for
787 confirmation that the user typed the password correctly. Generates
788 a field with name "name:confirm".
789 '''
790 return '<input type="password" name="%s:confirm" size="%s">'%(
791 self._name, size)
793 class NumberHTMLProperty(HTMLProperty):
794 def plain(self):
795 ''' Render a "plain" representation of the property
796 '''
797 return str(self._value)
799 def field(self, size = 30):
800 ''' Render a form edit field for the property
801 '''
802 if self._value is None:
803 value = ''
804 else:
805 value = cgi.escape(str(self._value))
806 value = '"'.join(value.split('"'))
807 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
809 class BooleanHTMLProperty(HTMLProperty):
810 def plain(self):
811 ''' Render a "plain" representation of the property
812 '''
813 if self.value is None:
814 return ''
815 return self._value and "Yes" or "No"
817 def field(self):
818 ''' Render a form edit field for the property
819 '''
820 checked = self._value and "checked" or ""
821 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
822 checked)
823 if checked:
824 checked = ""
825 else:
826 checked = "checked"
827 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
828 checked)
829 return s
831 class DateHTMLProperty(HTMLProperty):
832 def plain(self):
833 ''' Render a "plain" representation of the property
834 '''
835 if self._value is None:
836 return ''
837 return str(self._value)
839 def field(self, size = 30):
840 ''' Render a form edit field for the property
841 '''
842 if self._value is None:
843 value = ''
844 else:
845 value = cgi.escape(str(self._value))
846 value = '"'.join(value.split('"'))
847 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
849 def reldate(self, pretty=1):
850 ''' Render the interval between the date and now.
852 If the "pretty" flag is true, then make the display pretty.
853 '''
854 if not self._value:
855 return ''
857 # figure the interval
858 interval = date.Date('.') - self._value
859 if pretty:
860 return interval.pretty()
861 return str(interval)
863 class IntervalHTMLProperty(HTMLProperty):
864 def plain(self):
865 ''' Render a "plain" representation of the property
866 '''
867 if self._value is None:
868 return ''
869 return str(self._value)
871 def pretty(self):
872 ''' Render the interval in a pretty format (eg. "yesterday")
873 '''
874 return self._value.pretty()
876 def field(self, size = 30):
877 ''' Render a form edit field for the property
878 '''
879 if self._value is None:
880 value = ''
881 else:
882 value = cgi.escape(str(self._value))
883 value = '"'.join(value.split('"'))
884 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
886 class LinkHTMLProperty(HTMLProperty):
887 ''' Link HTMLProperty
888 Include the above as well as being able to access the class
889 information. Stringifying the object itself results in the value
890 from the item being displayed. Accessing attributes of this object
891 result in the appropriate entry from the class being queried for the
892 property accessed (so item/assignedto/name would look up the user
893 entry identified by the assignedto property on item, and then the
894 name property of that user)
895 '''
896 def __getattr__(self, attr):
897 ''' return a new HTMLItem '''
898 #print 'Link.getattr', (self, attr, self._value)
899 if not self._value:
900 raise AttributeError, "Can't access missing value"
901 if self._prop.classname == 'user':
902 klass = HTMLUser
903 else:
904 klass = HTMLItem
905 i = klass(self._client, self._prop.classname, self._value)
906 return getattr(i, attr)
908 def plain(self, escape=0):
909 ''' Render a "plain" representation of the property
910 '''
911 if self._value is None:
912 return ''
913 linkcl = self._db.classes[self._prop.classname]
914 k = linkcl.labelprop(1)
915 value = str(linkcl.get(self._value, k))
916 if escape:
917 value = cgi.escape(value)
918 return value
920 def field(self, showid=0, size=None):
921 ''' Render a form edit field for the property
922 '''
923 linkcl = self._db.getclass(self._prop.classname)
924 if linkcl.getprops().has_key('order'):
925 sort_on = 'order'
926 else:
927 sort_on = linkcl.labelprop()
928 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
929 # TODO: make this a field display, not a menu one!
930 l = ['<select name="%s">'%self._name]
931 k = linkcl.labelprop(1)
932 if self._value is None:
933 s = 'selected '
934 else:
935 s = ''
936 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
937 for optionid in options:
938 # get the option value, and if it's None use an empty string
939 option = linkcl.get(optionid, k) or ''
941 # figure if this option is selected
942 s = ''
943 if optionid == self._value:
944 s = 'selected '
946 # figure the label
947 if showid:
948 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
949 else:
950 lab = option
952 # truncate if it's too long
953 if size is not None and len(lab) > size:
954 lab = lab[:size-3] + '...'
956 # and generate
957 lab = cgi.escape(lab)
958 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
959 l.append('</select>')
960 return '\n'.join(l)
962 def menu(self, size=None, height=None, showid=0, additional=[],
963 **conditions):
964 ''' Render a form select list for this property
965 '''
966 value = self._value
968 # sort function
969 sortfunc = make_sort_function(self._db, self._prop.classname)
971 linkcl = self._db.getclass(self._prop.classname)
972 l = ['<select name="%s">'%self._name]
973 k = linkcl.labelprop(1)
974 s = ''
975 if value is None:
976 s = 'selected '
977 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
978 if linkcl.getprops().has_key('order'):
979 sort_on = ('+', 'order')
980 else:
981 sort_on = ('+', linkcl.labelprop())
982 options = linkcl.filter(None, conditions, sort_on, (None, None))
983 for optionid in options:
984 # get the option value, and if it's None use an empty string
985 option = linkcl.get(optionid, k) or ''
987 # figure if this option is selected
988 s = ''
989 if value in [optionid, option]:
990 s = 'selected '
992 # figure the label
993 if showid:
994 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
995 else:
996 lab = option
998 # truncate if it's too long
999 if size is not None and len(lab) > size:
1000 lab = lab[:size-3] + '...'
1001 if additional:
1002 m = []
1003 for propname in additional:
1004 m.append(linkcl.get(optionid, propname))
1005 lab = lab + ' (%s)'%', '.join(map(str, m))
1007 # and generate
1008 lab = cgi.escape(lab)
1009 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1010 l.append('</select>')
1011 return '\n'.join(l)
1012 # def checklist(self, ...)
1014 class MultilinkHTMLProperty(HTMLProperty):
1015 ''' Multilink HTMLProperty
1017 Also be iterable, returning a wrapper object like the Link case for
1018 each entry in the multilink.
1019 '''
1020 def __len__(self):
1021 ''' length of the multilink '''
1022 return len(self._value)
1024 def __getattr__(self, attr):
1025 ''' no extended attribute accesses make sense here '''
1026 raise AttributeError, attr
1028 def __getitem__(self, num):
1029 ''' iterate and return a new HTMLItem
1030 '''
1031 #print 'Multi.getitem', (self, num)
1032 value = self._value[num]
1033 if self._prop.classname == 'user':
1034 klass = HTMLUser
1035 else:
1036 klass = HTMLItem
1037 return klass(self._client, self._prop.classname, value)
1039 def __contains__(self, value):
1040 ''' Support the "in" operator
1041 '''
1042 return value in self._value
1044 def reverse(self):
1045 ''' return the list in reverse order
1046 '''
1047 l = self._value[:]
1048 l.reverse()
1049 if self._prop.classname == 'user':
1050 klass = HTMLUser
1051 else:
1052 klass = HTMLItem
1053 return [klass(self._client, self._prop.classname, value) for value in l]
1055 def plain(self, escape=0):
1056 ''' Render a "plain" representation of the property
1057 '''
1058 linkcl = self._db.classes[self._prop.classname]
1059 k = linkcl.labelprop(1)
1060 labels = []
1061 for v in self._value:
1062 labels.append(linkcl.get(v, k))
1063 value = ', '.join(labels)
1064 if escape:
1065 value = cgi.escape(value)
1066 return value
1068 def field(self, size=30, showid=0):
1069 ''' Render a form edit field for the property
1070 '''
1071 sortfunc = make_sort_function(self._db, self._prop.classname)
1072 linkcl = self._db.getclass(self._prop.classname)
1073 value = self._value[:]
1074 if value:
1075 value.sort(sortfunc)
1076 # map the id to the label property
1077 if not linkcl.getkey():
1078 showid=1
1079 if not showid:
1080 k = linkcl.labelprop(1)
1081 value = [linkcl.get(v, k) for v in value]
1082 value = cgi.escape(','.join(value))
1083 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1085 def menu(self, size=None, height=None, showid=0, additional=[],
1086 **conditions):
1087 ''' Render a form select list for this property
1088 '''
1089 value = self._value
1091 # sort function
1092 sortfunc = make_sort_function(self._db, self._prop.classname)
1094 linkcl = self._db.getclass(self._prop.classname)
1095 if linkcl.getprops().has_key('order'):
1096 sort_on = ('+', 'order')
1097 else:
1098 sort_on = ('+', linkcl.labelprop())
1099 options = linkcl.filter(None, conditions, sort_on, (None,None))
1100 height = height or min(len(options), 7)
1101 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1102 k = linkcl.labelprop(1)
1103 for optionid in options:
1104 # get the option value, and if it's None use an empty string
1105 option = linkcl.get(optionid, k) or ''
1107 # figure if this option is selected
1108 s = ''
1109 if optionid in value or option in value:
1110 s = 'selected '
1112 # figure the label
1113 if showid:
1114 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1115 else:
1116 lab = option
1117 # truncate if it's too long
1118 if size is not None and len(lab) > size:
1119 lab = lab[:size-3] + '...'
1120 if additional:
1121 m = []
1122 for propname in additional:
1123 m.append(linkcl.get(optionid, propname))
1124 lab = lab + ' (%s)'%', '.join(m)
1126 # and generate
1127 lab = cgi.escape(lab)
1128 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1129 lab))
1130 l.append('</select>')
1131 return '\n'.join(l)
1133 # set the propclasses for HTMLItem
1134 propclasses = (
1135 (hyperdb.String, StringHTMLProperty),
1136 (hyperdb.Number, NumberHTMLProperty),
1137 (hyperdb.Boolean, BooleanHTMLProperty),
1138 (hyperdb.Date, DateHTMLProperty),
1139 (hyperdb.Interval, IntervalHTMLProperty),
1140 (hyperdb.Password, PasswordHTMLProperty),
1141 (hyperdb.Link, LinkHTMLProperty),
1142 (hyperdb.Multilink, MultilinkHTMLProperty),
1143 )
1145 def make_sort_function(db, classname):
1146 '''Make a sort function for a given class
1147 '''
1148 linkcl = db.getclass(classname)
1149 if linkcl.getprops().has_key('order'):
1150 sort_on = 'order'
1151 else:
1152 sort_on = linkcl.labelprop()
1153 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1154 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1155 return sortfunc
1157 def handleListCGIValue(value):
1158 ''' Value is either a single item or a list of items. Each item has a
1159 .value that we're actually interested in.
1160 '''
1161 if isinstance(value, type([])):
1162 return [value.value for value in value]
1163 else:
1164 value = value.value.strip()
1165 if not value:
1166 return []
1167 return value.split(',')
1169 class ShowDict:
1170 ''' A convenience access to the :columns index parameters
1171 '''
1172 def __init__(self, columns):
1173 self.columns = {}
1174 for col in columns:
1175 self.columns[col] = 1
1176 def __getitem__(self, name):
1177 return self.columns.has_key(name)
1179 class HTMLRequest:
1180 ''' The *request*, holding the CGI form and environment.
1182 "form" the CGI form as a cgi.FieldStorage
1183 "env" the CGI environment variables
1184 "base" the base URL for this instance
1185 "user" a HTMLUser instance for this user
1186 "classname" the current classname (possibly None)
1187 "template" the current template (suffix, also possibly None)
1189 Index args:
1190 "columns" dictionary of the columns to display in an index page
1191 "show" a convenience access to columns - request/show/colname will
1192 be true if the columns should be displayed, false otherwise
1193 "sort" index sort column (direction, column name)
1194 "group" index grouping property (direction, column name)
1195 "filter" properties to filter the index on
1196 "filterspec" values to filter the index on
1197 "search_text" text to perform a full-text search on for an index
1199 '''
1200 def __init__(self, client):
1201 self.client = client
1203 # easier access vars
1204 self.form = client.form
1205 self.env = client.env
1206 self.base = client.base
1207 self.user = HTMLUser(client, 'user', client.userid)
1209 # store the current class name and action
1210 self.classname = client.classname
1211 self.template = client.template
1213 self._post_init()
1215 def _post_init(self):
1216 ''' Set attributes based on self.form
1217 '''
1218 # extract the index display information from the form
1219 self.columns = []
1220 if self.form.has_key(':columns'):
1221 self.columns = handleListCGIValue(self.form[':columns'])
1222 self.show = ShowDict(self.columns)
1224 # sorting
1225 self.sort = (None, None)
1226 if self.form.has_key(':sort'):
1227 sort = self.form[':sort'].value
1228 if sort.startswith('-'):
1229 self.sort = ('-', sort[1:])
1230 else:
1231 self.sort = ('+', sort)
1232 if self.form.has_key(':sortdir'):
1233 self.sort = ('-', self.sort[1])
1235 # grouping
1236 self.group = (None, None)
1237 if self.form.has_key(':group'):
1238 group = self.form[':group'].value
1239 if group.startswith('-'):
1240 self.group = ('-', group[1:])
1241 else:
1242 self.group = ('+', group)
1243 if self.form.has_key(':groupdir'):
1244 self.group = ('-', self.group[1])
1246 # filtering
1247 self.filter = []
1248 if self.form.has_key(':filter'):
1249 self.filter = handleListCGIValue(self.form[':filter'])
1250 self.filterspec = {}
1251 db = self.client.db
1252 if self.classname is not None:
1253 props = db.getclass(self.classname).getprops()
1254 for name in self.filter:
1255 if self.form.has_key(name):
1256 prop = props[name]
1257 fv = self.form[name]
1258 if (isinstance(prop, hyperdb.Link) or
1259 isinstance(prop, hyperdb.Multilink)):
1260 self.filterspec[name] = lookupIds(db, prop,
1261 handleListCGIValue(fv))
1262 else:
1263 self.filterspec[name] = fv.value
1265 # full-text search argument
1266 self.search_text = None
1267 if self.form.has_key(':search_text'):
1268 self.search_text = self.form[':search_text'].value
1270 # pagination - size and start index
1271 # figure batch args
1272 if self.form.has_key(':pagesize'):
1273 self.pagesize = int(self.form[':pagesize'].value)
1274 else:
1275 self.pagesize = 50
1276 if self.form.has_key(':startwith'):
1277 self.startwith = int(self.form[':startwith'].value)
1278 else:
1279 self.startwith = 0
1281 def updateFromURL(self, url):
1282 ''' Parse the URL for query args, and update my attributes using the
1283 values.
1284 '''
1285 self.form = {}
1286 for name, value in cgi.parse_qsl(url):
1287 if self.form.has_key(name):
1288 if isinstance(self.form[name], type([])):
1289 self.form[name].append(cgi.MiniFieldStorage(name, value))
1290 else:
1291 self.form[name] = [self.form[name],
1292 cgi.MiniFieldStorage(name, value)]
1293 else:
1294 self.form[name] = cgi.MiniFieldStorage(name, value)
1295 self._post_init()
1297 def update(self, kwargs):
1298 ''' Update my attributes using the keyword args
1299 '''
1300 self.__dict__.update(kwargs)
1301 if kwargs.has_key('columns'):
1302 self.show = ShowDict(self.columns)
1304 def description(self):
1305 ''' Return a description of the request - handle for the page title.
1306 '''
1307 s = [self.client.db.config.TRACKER_NAME]
1308 if self.classname:
1309 if self.client.nodeid:
1310 s.append('- %s%s'%(self.classname, self.client.nodeid))
1311 else:
1312 if self.template == 'item':
1313 s.append('- new %s'%self.classname)
1314 elif self.template == 'index':
1315 s.append('- %s index'%self.classname)
1316 else:
1317 s.append('- %s %s'%(self.classname, self.template))
1318 else:
1319 s.append('- home')
1320 return ' '.join(s)
1322 def __str__(self):
1323 d = {}
1324 d.update(self.__dict__)
1325 f = ''
1326 for k in self.form.keys():
1327 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1328 d['form'] = f
1329 e = ''
1330 for k,v in self.env.items():
1331 e += '\n %r=%r'%(k, v)
1332 d['env'] = e
1333 return '''
1334 form: %(form)s
1335 base: %(base)r
1336 classname: %(classname)r
1337 template: %(template)r
1338 columns: %(columns)r
1339 sort: %(sort)r
1340 group: %(group)r
1341 filter: %(filter)r
1342 search_text: %(search_text)r
1343 pagesize: %(pagesize)r
1344 startwith: %(startwith)r
1345 env: %(env)s
1346 '''%d
1348 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1349 filterspec=1):
1350 ''' return the current index args as form elements '''
1351 l = []
1352 s = '<input type="hidden" name="%s" value="%s">'
1353 if columns and self.columns:
1354 l.append(s%(':columns', ','.join(self.columns)))
1355 if sort and self.sort[1] is not None:
1356 if self.sort[0] == '-':
1357 val = '-'+self.sort[1]
1358 else:
1359 val = self.sort[1]
1360 l.append(s%(':sort', val))
1361 if group and self.group[1] is not None:
1362 if self.group[0] == '-':
1363 val = '-'+self.group[1]
1364 else:
1365 val = self.group[1]
1366 l.append(s%(':group', val))
1367 if filter and self.filter:
1368 l.append(s%(':filter', ','.join(self.filter)))
1369 if filterspec:
1370 for k,v in self.filterspec.items():
1371 l.append(s%(k, ','.join(v)))
1372 if self.search_text:
1373 l.append(s%(':search_text', self.search_text))
1374 l.append(s%(':pagesize', self.pagesize))
1375 l.append(s%(':startwith', self.startwith))
1376 return '\n'.join(l)
1378 def indexargs_url(self, url, args):
1379 ''' embed the current index args in a URL '''
1380 l = ['%s=%s'%(k,v) for k,v in args.items()]
1381 if self.columns and not args.has_key(':columns'):
1382 l.append(':columns=%s'%(','.join(self.columns)))
1383 if self.sort[1] is not None and not args.has_key(':sort'):
1384 if self.sort[0] == '-':
1385 val = '-'+self.sort[1]
1386 else:
1387 val = self.sort[1]
1388 l.append(':sort=%s'%val)
1389 if self.group[1] is not None and not args.has_key(':group'):
1390 if self.group[0] == '-':
1391 val = '-'+self.group[1]
1392 else:
1393 val = self.group[1]
1394 l.append(':group=%s'%val)
1395 if self.filter and not args.has_key(':columns'):
1396 l.append(':filter=%s'%(','.join(self.filter)))
1397 for k,v in self.filterspec.items():
1398 if not args.has_key(k):
1399 l.append('%s=%s'%(k, ','.join(v)))
1400 if self.search_text and not args.has_key(':search_text'):
1401 l.append(':search_text=%s'%self.search_text)
1402 if not args.has_key(':pagesize'):
1403 l.append(':pagesize=%s'%self.pagesize)
1404 if not args.has_key(':startwith'):
1405 l.append(':startwith=%s'%self.startwith)
1406 return '%s?%s'%(url, '&'.join(l))
1407 indexargs_href = indexargs_url
1409 def base_javascript(self):
1410 return '''
1411 <script language="javascript">
1412 submitted = false;
1413 function submit_once() {
1414 if (submitted) {
1415 alert("Your request is being processed.\\nPlease be patient.");
1416 return 0;
1417 }
1418 submitted = true;
1419 return 1;
1420 }
1422 function help_window(helpurl, width, height) {
1423 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1424 }
1425 </script>
1426 '''%self.base
1428 def batch(self):
1429 ''' Return a batch object for results from the "current search"
1430 '''
1431 filterspec = self.filterspec
1432 sort = self.sort
1433 group = self.group
1435 # get the list of ids we're batching over
1436 klass = self.client.db.getclass(self.classname)
1437 if self.search_text:
1438 matches = self.client.db.indexer.search(
1439 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1440 else:
1441 matches = None
1442 l = klass.filter(matches, filterspec, sort, group)
1444 # return the batch object, using IDs only
1445 return Batch(self.client, l, self.pagesize, self.startwith,
1446 classname=self.classname)
1448 # extend the standard ZTUtils Batch object to remove dependency on
1449 # Acquisition and add a couple of useful methods
1450 class Batch(ZTUtils.Batch):
1451 ''' Use me to turn a list of items, or item ids of a given class, into a
1452 series of batches.
1454 ========= ========================================================
1455 Parameter Usage
1456 ========= ========================================================
1457 sequence a list of HTMLItems or item ids
1458 classname if sequence is a list of ids, this is the class of item
1459 size how big to make the sequence.
1460 start where to start (0-indexed) in the sequence.
1461 end where to end (0-indexed) in the sequence.
1462 orphan if the next batch would contain less items than this
1463 value, then it is combined with this batch
1464 overlap the number of items shared between adjacent batches
1465 ========= ========================================================
1467 Attributes: Note that the "start" attribute, unlike the
1468 argument, is a 1-based index (I know, lame). "first" is the
1469 0-based index. "length" is the actual number of elements in
1470 the batch.
1472 "sequence_length" is the length of the original, unbatched, sequence.
1473 '''
1474 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1475 overlap=0, classname=None):
1476 self.client = client
1477 self.last_index = self.last_item = None
1478 self.current_item = None
1479 self.classname = classname
1480 self.sequence_length = len(sequence)
1481 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1482 overlap)
1484 # overwrite so we can late-instantiate the HTMLItem instance
1485 def __getitem__(self, index):
1486 if index < 0:
1487 if index + self.end < self.first: raise IndexError, index
1488 return self._sequence[index + self.end]
1490 if index >= self.length:
1491 raise IndexError, index
1493 # move the last_item along - but only if the fetched index changes
1494 # (for some reason, index 0 is fetched twice)
1495 if index != self.last_index:
1496 self.last_item = self.current_item
1497 self.last_index = index
1499 item = self._sequence[index + self.first]
1500 if self.classname:
1501 # map the item ids to instances
1502 if self.classname == 'user':
1503 item = HTMLUser(self.client, self.classname, item)
1504 else:
1505 item = HTMLItem(self.client, self.classname, item)
1506 self.current_item = item
1507 return item
1509 def propchanged(self, property):
1510 ''' Detect if the property marked as being the group property
1511 changed in the last iteration fetch
1512 '''
1513 if (self.last_item is None or
1514 self.last_item[property] != self.current_item[property]):
1515 return 1
1516 return 0
1518 # override these 'cos we don't have access to acquisition
1519 def previous(self):
1520 if self.start == 1:
1521 return None
1522 return Batch(self.client, self._sequence, self._size,
1523 self.first - self._size + self.overlap, 0, self.orphan,
1524 self.overlap)
1526 def next(self):
1527 try:
1528 self._sequence[self.end]
1529 except IndexError:
1530 return None
1531 return Batch(self.client, self._sequence, self._size,
1532 self.end - self.overlap, 0, self.orphan, self.overlap)
1534 class TemplatingUtils:
1535 ''' Utilities for templating
1536 '''
1537 def __init__(self, client):
1538 self.client = client
1539 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1540 return Batch(self.client, sequence, size, start, end, orphan,
1541 overlap)