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 *config*
130 The current tracker config.
131 *db*
132 The current database, used to access arbitrary database items.
133 '''
134 def getContext(self, client, classname, request):
135 c = {
136 'options': {},
137 'nothing': None,
138 'request': request,
139 'db': HTMLDatabase(client),
140 'config': client.instance.config,
141 'tracker': client.instance,
142 'utils': TemplatingUtils(client),
143 'templates': Templates(client.instance.config.TEMPLATES),
144 }
145 # add in the item if there is one
146 if client.nodeid:
147 if classname == 'user':
148 c['context'] = HTMLUser(client, classname, client.nodeid)
149 else:
150 c['context'] = HTMLItem(client, classname, client.nodeid)
151 elif client.db.classes.has_key(classname):
152 c['context'] = HTMLClass(client, classname)
153 return c
155 def render(self, client, classname, request, **options):
156 """Render this Page Template"""
158 if not self._v_cooked:
159 self._cook()
161 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
163 if self._v_errors:
164 raise PageTemplate.PTRuntimeError, \
165 'Page Template %s has errors.'%self.id
167 # figure the context
168 classname = classname or client.classname
169 request = request or HTMLRequest(client)
170 c = self.getContext(client, classname, request)
171 c.update({'options': options})
173 # and go
174 output = StringIO.StringIO()
175 TALInterpreter(self._v_program, self.macros,
176 getEngine().getContext(c), output, tal=1, strictinsert=0)()
177 return output.getvalue()
179 class HTMLDatabase:
180 ''' Return HTMLClasses for valid class fetches
181 '''
182 def __init__(self, client):
183 self._client = client
185 # we want config to be exposed
186 self.config = client.db.config
188 def __getitem__(self, item):
189 self._client.db.getclass(item)
190 return HTMLClass(self._client, item)
192 def __getattr__(self, attr):
193 try:
194 return self[attr]
195 except KeyError:
196 raise AttributeError, attr
198 def classes(self):
199 l = self._client.db.classes.keys()
200 l.sort()
201 return [HTMLClass(self._client, cn) for cn in l]
203 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
204 cl = db.getclass(prop.classname)
205 l = []
206 for entry in ids:
207 if num_re.match(entry):
208 l.append(entry)
209 else:
210 try:
211 l.append(cl.lookup(entry))
212 except KeyError:
213 # ignore invalid keys
214 pass
215 return l
217 class HTMLPermissions:
218 ''' Helpers that provide answers to commonly asked Permission questions.
219 '''
220 def is_edit_ok(self):
221 ''' Is the user allowed to Edit the current class?
222 '''
223 return self._db.security.hasPermission('Edit', self._client.userid,
224 self._classname)
225 def is_view_ok(self):
226 ''' Is the user allowed to View the current class?
227 '''
228 return self._db.security.hasPermission('View', self._client.userid,
229 self._classname)
230 def is_only_view_ok(self):
231 ''' Is the user only allowed to View (ie. not Edit) the current class?
232 '''
233 return self.is_view_ok() and not self.is_edit_ok()
235 class HTMLClass(HTMLPermissions):
236 ''' Accesses through a class (either through *class* or *db.<classname>*)
237 '''
238 def __init__(self, client, classname):
239 self._client = client
240 self._db = client.db
242 # we want classname to be exposed, but _classname gives a
243 # consistent API for extending Class/Item
244 self._classname = self.classname = classname
245 self._klass = self._db.getclass(self.classname)
246 self._props = self._klass.getprops()
248 def __repr__(self):
249 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
251 def __getitem__(self, item):
252 ''' return an HTMLProperty instance
253 '''
254 #print 'HTMLClass.getitem', (self, item)
256 # we don't exist
257 if item == 'id':
258 return None
260 # get the property
261 prop = self._props[item]
263 # look up the correct HTMLProperty class
264 form = self._client.form
265 for klass, htmlklass in propclasses:
266 if not isinstance(prop, klass):
267 continue
268 if form.has_key(item):
269 if isinstance(prop, hyperdb.Multilink):
270 value = lookupIds(self._db, prop,
271 handleListCGIValue(form[item]))
272 elif isinstance(prop, hyperdb.Link):
273 value = form[item].value.strip()
274 if value:
275 value = lookupIds(self._db, prop, [value])[0]
276 else:
277 value = None
278 else:
279 value = form[item].value.strip() or None
280 else:
281 if isinstance(prop, hyperdb.Multilink):
282 value = []
283 else:
284 value = None
285 return htmlklass(self._client, '', prop, item, value)
287 # no good
288 raise KeyError, item
290 def __getattr__(self, attr):
291 ''' convenience access '''
292 try:
293 return self[attr]
294 except KeyError:
295 raise AttributeError, attr
297 def getItem(self, itemid, num_re=re.compile('\d+')):
298 ''' Get an item of this class by its item id.
299 '''
300 # make sure we're looking at an itemid
301 if not num_re.match(itemid):
302 itemid = self._klass.lookup(itemid)
304 if self.classname == 'user':
305 klass = HTMLUser
306 else:
307 klass = HTMLItem
309 return klass(self._client, self.classname, itemid)
311 def properties(self):
312 ''' Return HTMLProperty for all of this class' properties.
313 '''
314 l = []
315 for name, prop in self._props.items():
316 for klass, htmlklass in propclasses:
317 if isinstance(prop, hyperdb.Multilink):
318 value = []
319 else:
320 value = None
321 if isinstance(prop, klass):
322 l.append(htmlklass(self._client, '', prop, name, value))
323 return l
325 def list(self):
326 ''' List all items in this class.
327 '''
328 if self.classname == 'user':
329 klass = HTMLUser
330 else:
331 klass = HTMLItem
333 # get the list and sort it nicely
334 l = self._klass.list()
335 sortfunc = make_sort_function(self._db, self.classname)
336 l.sort(sortfunc)
338 l = [klass(self._client, self.classname, x) for x in l]
339 return l
341 def csv(self):
342 ''' Return the items of this class as a chunk of CSV text.
343 '''
344 # get the CSV module
345 try:
346 import csv
347 except ImportError:
348 return 'Sorry, you need the csv module to use this function.\n'\
349 'Get it from: http://www.object-craft.com.au/projects/csv/'
351 props = self.propnames()
352 p = csv.parser()
353 s = StringIO.StringIO()
354 s.write(p.join(props) + '\n')
355 for nodeid in self._klass.list():
356 l = []
357 for name in props:
358 value = self._klass.get(nodeid, name)
359 if value is None:
360 l.append('')
361 elif isinstance(value, type([])):
362 l.append(':'.join(map(str, value)))
363 else:
364 l.append(str(self._klass.get(nodeid, name)))
365 s.write(p.join(l) + '\n')
366 return s.getvalue()
368 def propnames(self):
369 ''' Return the list of the names of the properties of this class.
370 '''
371 idlessprops = self._klass.getprops(protected=0).keys()
372 idlessprops.sort()
373 return ['id'] + idlessprops
375 def filter(self, request=None):
376 ''' Return a list of items from this class, filtered and sorted
377 by the current requested filterspec/filter/sort/group args
378 '''
379 if request is not None:
380 filterspec = request.filterspec
381 sort = request.sort
382 group = request.group
383 if self.classname == 'user':
384 klass = HTMLUser
385 else:
386 klass = HTMLItem
387 l = [klass(self._client, self.classname, x)
388 for x in self._klass.filter(None, filterspec, sort, group)]
389 return l
391 def classhelp(self, properties=None, label='list', width='500',
392 height='400'):
393 ''' Pop up a javascript window with class help
395 This generates a link to a popup window which displays the
396 properties indicated by "properties" of the class named by
397 "classname". The "properties" should be a comma-separated list
398 (eg. 'id,name,description'). Properties defaults to all the
399 properties of a class (excluding id, creator, created and
400 activity).
402 You may optionally override the label displayed, the width and
403 height. The popup window will be resizable and scrollable.
404 '''
405 if properties is None:
406 properties = self._klass.getprops(protected=0).keys()
407 properties.sort()
408 properties = ','.join(properties)
409 return '<a href="javascript:help_window(\'%s?:template=help&' \
410 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(
411 self.classname, properties, width, height, label)
413 def submit(self, label="Submit New Entry"):
414 ''' Generate a submit button (and action hidden element)
415 '''
416 return ' <input type="hidden" name=":action" value="new">\n'\
417 ' <input type="submit" name="submit" value="%s">'%label
419 def history(self):
420 return 'New node - no history'
422 def renderWith(self, name, **kwargs):
423 ''' Render this class with the given template.
424 '''
425 # create a new request and override the specified args
426 req = HTMLRequest(self._client)
427 req.classname = self.classname
428 req.update(kwargs)
430 # new template, using the specified classname and request
431 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
433 # use our fabricated request
434 return pt.render(self._client, self.classname, req)
436 class HTMLItem(HTMLPermissions):
437 ''' Accesses through an *item*
438 '''
439 def __init__(self, client, classname, nodeid):
440 self._client = client
441 self._db = client.db
442 self._classname = classname
443 self._nodeid = nodeid
444 self._klass = self._db.getclass(classname)
445 self._props = self._klass.getprops()
447 def __repr__(self):
448 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
449 self._nodeid)
451 def __getitem__(self, item):
452 ''' return an HTMLProperty instance
453 '''
454 #print 'HTMLItem.getitem', (self, item)
455 if item == 'id':
456 return self._nodeid
458 # get the property
459 prop = self._props[item]
461 # get the value, handling missing values
462 value = self._klass.get(self._nodeid, item, None)
463 if value is None:
464 if isinstance(self._props[item], hyperdb.Multilink):
465 value = []
467 # look up the correct HTMLProperty class
468 for klass, htmlklass in propclasses:
469 if isinstance(prop, klass):
470 return htmlklass(self._client, self._nodeid, prop, item, value)
472 raise KeyErorr, item
474 def __getattr__(self, attr):
475 ''' convenience access to properties '''
476 try:
477 return self[attr]
478 except KeyError:
479 raise AttributeError, attr
481 def submit(self, label="Submit Changes"):
482 ''' Generate a submit button (and action hidden element)
483 '''
484 return ' <input type="hidden" name=":action" value="edit">\n'\
485 ' <input type="submit" name="submit" value="%s">'%label
487 def journal(self, direction='descending'):
488 ''' Return a list of HTMLJournalEntry instances.
489 '''
490 # XXX do this
491 return []
493 def history(self, direction='descending', dre=re.compile('\d+')):
494 l = ['<table class="history">'
495 '<tr><th colspan="4" class="header">',
496 _('History'),
497 '</th></tr><tr>',
498 _('<th>Date</th>'),
499 _('<th>User</th>'),
500 _('<th>Action</th>'),
501 _('<th>Args</th>'),
502 '</tr>']
503 comments = {}
504 history = self._klass.history(self._nodeid)
505 history.sort()
506 if direction == 'descending':
507 history.reverse()
508 for id, evt_date, user, action, args in history:
509 date_s = str(evt_date).replace("."," ")
510 arg_s = ''
511 if action == 'link' and type(args) == type(()):
512 if len(args) == 3:
513 linkcl, linkid, key = args
514 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
515 linkcl, linkid, key)
516 else:
517 arg_s = str(args)
519 elif action == 'unlink' and type(args) == type(()):
520 if len(args) == 3:
521 linkcl, linkid, key = args
522 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
523 linkcl, linkid, key)
524 else:
525 arg_s = str(args)
527 elif type(args) == type({}):
528 cell = []
529 for k in args.keys():
530 # try to get the relevant property and treat it
531 # specially
532 try:
533 prop = self._props[k]
534 except KeyError:
535 prop = None
536 if prop is not None:
537 if args[k] and (isinstance(prop, hyperdb.Multilink) or
538 isinstance(prop, hyperdb.Link)):
539 # figure what the link class is
540 classname = prop.classname
541 try:
542 linkcl = self._db.getclass(classname)
543 except KeyError:
544 labelprop = None
545 comments[classname] = _('''The linked class
546 %(classname)s no longer exists''')%locals()
547 labelprop = linkcl.labelprop(1)
548 hrefable = os.path.exists(
549 os.path.join(self._db.config.TEMPLATES,
550 classname+'.item'))
552 if isinstance(prop, hyperdb.Multilink) and \
553 len(args[k]) > 0:
554 ml = []
555 for linkid in args[k]:
556 if isinstance(linkid, type(())):
557 sublabel = linkid[0] + ' '
558 linkids = linkid[1]
559 else:
560 sublabel = ''
561 linkids = [linkid]
562 subml = []
563 for linkid in linkids:
564 label = classname + linkid
565 # if we have a label property, try to use it
566 # TODO: test for node existence even when
567 # there's no labelprop!
568 try:
569 if labelprop is not None:
570 label = linkcl.get(linkid, labelprop)
571 except IndexError:
572 comments['no_link'] = _('''<strike>The
573 linked node no longer
574 exists</strike>''')
575 subml.append('<strike>%s</strike>'%label)
576 else:
577 if hrefable:
578 subml.append('<a href="%s%s">%s</a>'%(
579 classname, linkid, label))
580 ml.append(sublabel + ', '.join(subml))
581 cell.append('%s:\n %s'%(k, ', '.join(ml)))
582 elif isinstance(prop, hyperdb.Link) and args[k]:
583 label = classname + args[k]
584 # if we have a label property, try to use it
585 # TODO: test for node existence even when
586 # there's no labelprop!
587 if labelprop is not None:
588 try:
589 label = linkcl.get(args[k], labelprop)
590 except IndexError:
591 comments['no_link'] = _('''<strike>The
592 linked node no longer
593 exists</strike>''')
594 cell.append(' <strike>%s</strike>,\n'%label)
595 # "flag" this is done .... euwww
596 label = None
597 if label is not None:
598 if hrefable:
599 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
600 classname, args[k], label))
601 else:
602 cell.append('%s: %s' % (k,label))
604 elif isinstance(prop, hyperdb.Date) and args[k]:
605 d = date.Date(args[k])
606 cell.append('%s: %s'%(k, str(d)))
608 elif isinstance(prop, hyperdb.Interval) and args[k]:
609 d = date.Interval(args[k])
610 cell.append('%s: %s'%(k, str(d)))
612 elif isinstance(prop, hyperdb.String) and args[k]:
613 cell.append('%s: %s'%(k, cgi.escape(args[k])))
615 elif not args[k]:
616 cell.append('%s: (no value)\n'%k)
618 else:
619 cell.append('%s: %s\n'%(k, str(args[k])))
620 else:
621 # property no longer exists
622 comments['no_exist'] = _('''<em>The indicated property
623 no longer exists</em>''')
624 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
625 arg_s = '<br />'.join(cell)
626 else:
627 # unkown event!!
628 comments['unknown'] = _('''<strong><em>This event is not
629 handled by the history display!</em></strong>''')
630 arg_s = '<strong><em>' + str(args) + '</em></strong>'
631 date_s = date_s.replace(' ', ' ')
632 # if the user's an itemid, figure the username (older journals
633 # have the username)
634 if dre.match(user):
635 user = self._db.user.get(user, 'username')
636 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
637 date_s, user, action, arg_s))
638 if comments:
639 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
640 for entry in comments.values():
641 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
642 l.append('</table>')
643 return '\n'.join(l)
645 def renderQueryForm(self):
646 ''' Render this item, which is a query, as a search form.
647 '''
648 # create a new request and override the specified args
649 req = HTMLRequest(self._client)
650 req.classname = self._klass.get(self._nodeid, 'klass')
651 req.updateFromURL(self._klass.get(self._nodeid, 'url'))
653 # new template, using the specified classname and request
654 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
656 # use our fabricated request
657 return pt.render(self._client, req.classname, req)
659 class HTMLUser(HTMLItem):
660 ''' Accesses through the *user* (a special case of item)
661 '''
662 def __init__(self, client, classname, nodeid):
663 HTMLItem.__init__(self, client, 'user', nodeid)
664 self._default_classname = client.classname
666 # used for security checks
667 self._security = client.db.security
669 _marker = []
670 def hasPermission(self, role, classname=_marker):
671 ''' Determine if the user has the Role.
673 The class being tested defaults to the template's class, but may
674 be overidden for this test by suppling an alternate classname.
675 '''
676 if classname is self._marker:
677 classname = self._default_classname
678 return self._security.hasPermission(role, self._nodeid, classname)
680 def is_edit_ok(self):
681 ''' Is the user allowed to Edit the current class?
682 Also check whether this is the current user's info.
683 '''
684 return self._db.security.hasPermission('Edit', self._client.userid,
685 self._classname) or self._nodeid == self._client.userid
687 def is_view_ok(self):
688 ''' Is the user allowed to View the current class?
689 Also check whether this is the current user's info.
690 '''
691 return self._db.security.hasPermission('Edit', self._client.userid,
692 self._classname) or self._nodeid == self._client.userid
694 class HTMLProperty:
695 ''' String, Number, Date, Interval HTMLProperty
697 Has useful attributes:
699 _name the name of the property
700 _value the value of the property if any
702 A wrapper object which may be stringified for the plain() behaviour.
703 '''
704 def __init__(self, client, nodeid, prop, name, value):
705 self._client = client
706 self._db = client.db
707 self._nodeid = nodeid
708 self._prop = prop
709 self._name = name
710 self._value = value
711 def __repr__(self):
712 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
713 def __str__(self):
714 return self.plain()
715 def __cmp__(self, other):
716 if isinstance(other, HTMLProperty):
717 return cmp(self._value, other._value)
718 return cmp(self._value, other)
720 class StringHTMLProperty(HTMLProperty):
721 def plain(self, escape=0):
722 ''' Render a "plain" representation of the property
723 '''
724 if self._value is None:
725 return ''
726 if escape:
727 return cgi.escape(str(self._value))
728 return str(self._value)
730 def stext(self, escape=0):
731 ''' Render the value of the property as StructuredText.
733 This requires the StructureText module to be installed separately.
734 '''
735 s = self.plain(escape=escape)
736 if not StructuredText:
737 return s
738 return StructuredText(s,level=1,header=0)
740 def field(self, size = 30):
741 ''' Render a form edit field for the property
742 '''
743 if self._value is None:
744 value = ''
745 else:
746 value = cgi.escape(str(self._value))
747 value = '"'.join(value.split('"'))
748 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
750 def multiline(self, escape=0, rows=5, cols=40):
751 ''' Render a multiline form edit field for the property
752 '''
753 if self._value is None:
754 value = ''
755 else:
756 value = cgi.escape(str(self._value))
757 value = '"'.join(value.split('"'))
758 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
759 self._name, rows, cols, value)
761 def email(self, escape=1):
762 ''' Render the value of the property as an obscured email address
763 '''
764 if self._value is None: value = ''
765 else: value = str(self._value)
766 if value.find('@') != -1:
767 name, domain = value.split('@')
768 domain = ' '.join(domain.split('.')[:-1])
769 name = name.replace('.', ' ')
770 value = '%s at %s ...'%(name, domain)
771 else:
772 value = value.replace('.', ' ')
773 if escape:
774 value = cgi.escape(value)
775 return value
777 class PasswordHTMLProperty(HTMLProperty):
778 def plain(self):
779 ''' Render a "plain" representation of the property
780 '''
781 if self._value is None:
782 return ''
783 return _('*encrypted*')
785 def field(self, size = 30):
786 ''' Render a form edit field for the property.
787 '''
788 return '<input type="password" name="%s" size="%s">'%(self._name, size)
790 def confirm(self, size = 30):
791 ''' Render a second form edit field for the property, used for
792 confirmation that the user typed the password correctly. Generates
793 a field with name "name:confirm".
794 '''
795 return '<input type="password" name="%s:confirm" size="%s">'%(
796 self._name, size)
798 class NumberHTMLProperty(HTMLProperty):
799 def plain(self):
800 ''' Render a "plain" representation of the property
801 '''
802 return str(self._value)
804 def field(self, size = 30):
805 ''' Render a form edit field for the property
806 '''
807 if self._value is None:
808 value = ''
809 else:
810 value = cgi.escape(str(self._value))
811 value = '"'.join(value.split('"'))
812 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
814 class BooleanHTMLProperty(HTMLProperty):
815 def plain(self):
816 ''' Render a "plain" representation of the property
817 '''
818 if self.value is None:
819 return ''
820 return self._value and "Yes" or "No"
822 def field(self):
823 ''' Render a form edit field for the property
824 '''
825 checked = self._value and "checked" or ""
826 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
827 checked)
828 if checked:
829 checked = ""
830 else:
831 checked = "checked"
832 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
833 checked)
834 return s
836 class DateHTMLProperty(HTMLProperty):
837 def plain(self):
838 ''' Render a "plain" representation of the property
839 '''
840 if self._value is None:
841 return ''
842 return str(self._value)
844 def field(self, size = 30):
845 ''' Render a form edit field for the property
846 '''
847 if self._value is None:
848 value = ''
849 else:
850 value = cgi.escape(str(self._value))
851 value = '"'.join(value.split('"'))
852 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
854 def reldate(self, pretty=1):
855 ''' Render the interval between the date and now.
857 If the "pretty" flag is true, then make the display pretty.
858 '''
859 if not self._value:
860 return ''
862 # figure the interval
863 interval = date.Date('.') - self._value
864 if pretty:
865 return interval.pretty()
866 return str(interval)
868 class IntervalHTMLProperty(HTMLProperty):
869 def plain(self):
870 ''' Render a "plain" representation of the property
871 '''
872 if self._value is None:
873 return ''
874 return str(self._value)
876 def pretty(self):
877 ''' Render the interval in a pretty format (eg. "yesterday")
878 '''
879 return self._value.pretty()
881 def field(self, size = 30):
882 ''' Render a form edit field for the property
883 '''
884 if self._value is None:
885 value = ''
886 else:
887 value = cgi.escape(str(self._value))
888 value = '"'.join(value.split('"'))
889 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
891 class LinkHTMLProperty(HTMLProperty):
892 ''' Link HTMLProperty
893 Include the above as well as being able to access the class
894 information. Stringifying the object itself results in the value
895 from the item being displayed. Accessing attributes of this object
896 result in the appropriate entry from the class being queried for the
897 property accessed (so item/assignedto/name would look up the user
898 entry identified by the assignedto property on item, and then the
899 name property of that user)
900 '''
901 def __getattr__(self, attr):
902 ''' return a new HTMLItem '''
903 #print 'Link.getattr', (self, attr, self._value)
904 if not self._value:
905 raise AttributeError, "Can't access missing value"
906 if self._prop.classname == 'user':
907 klass = HTMLUser
908 else:
909 klass = HTMLItem
910 i = klass(self._client, self._prop.classname, self._value)
911 return getattr(i, attr)
913 def plain(self, escape=0):
914 ''' Render a "plain" representation of the property
915 '''
916 if self._value is None:
917 return ''
918 linkcl = self._db.classes[self._prop.classname]
919 k = linkcl.labelprop(1)
920 value = str(linkcl.get(self._value, k))
921 if escape:
922 value = cgi.escape(value)
923 return value
925 def field(self, showid=0, size=None):
926 ''' Render a form edit field for the property
927 '''
928 linkcl = self._db.getclass(self._prop.classname)
929 if linkcl.getprops().has_key('order'):
930 sort_on = 'order'
931 else:
932 sort_on = linkcl.labelprop()
933 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
934 # TODO: make this a field display, not a menu one!
935 l = ['<select name="%s">'%self._name]
936 k = linkcl.labelprop(1)
937 if self._value is None:
938 s = 'selected '
939 else:
940 s = ''
941 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
942 for optionid in options:
943 # get the option value, and if it's None use an empty string
944 option = linkcl.get(optionid, k) or ''
946 # figure if this option is selected
947 s = ''
948 if optionid == self._value:
949 s = 'selected '
951 # figure the label
952 if showid:
953 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
954 else:
955 lab = option
957 # truncate if it's too long
958 if size is not None and len(lab) > size:
959 lab = lab[:size-3] + '...'
961 # and generate
962 lab = cgi.escape(lab)
963 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
964 l.append('</select>')
965 return '\n'.join(l)
967 def menu(self, size=None, height=None, showid=0, additional=[],
968 **conditions):
969 ''' Render a form select list for this property
970 '''
971 value = self._value
973 # sort function
974 sortfunc = make_sort_function(self._db, self._prop.classname)
976 linkcl = self._db.getclass(self._prop.classname)
977 l = ['<select name="%s">'%self._name]
978 k = linkcl.labelprop(1)
979 s = ''
980 if value is None:
981 s = 'selected '
982 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
983 if linkcl.getprops().has_key('order'):
984 sort_on = ('+', 'order')
985 else:
986 sort_on = ('+', linkcl.labelprop())
987 options = linkcl.filter(None, conditions, sort_on, (None, None))
988 for optionid in options:
989 # get the option value, and if it's None use an empty string
990 option = linkcl.get(optionid, k) or ''
992 # figure if this option is selected
993 s = ''
994 if value in [optionid, option]:
995 s = 'selected '
997 # figure the label
998 if showid:
999 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1000 else:
1001 lab = option
1003 # truncate if it's too long
1004 if size is not None and len(lab) > size:
1005 lab = lab[:size-3] + '...'
1006 if additional:
1007 m = []
1008 for propname in additional:
1009 m.append(linkcl.get(optionid, propname))
1010 lab = lab + ' (%s)'%', '.join(map(str, m))
1012 # and generate
1013 lab = cgi.escape(lab)
1014 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1015 l.append('</select>')
1016 return '\n'.join(l)
1017 # def checklist(self, ...)
1019 class MultilinkHTMLProperty(HTMLProperty):
1020 ''' Multilink HTMLProperty
1022 Also be iterable, returning a wrapper object like the Link case for
1023 each entry in the multilink.
1024 '''
1025 def __len__(self):
1026 ''' length of the multilink '''
1027 return len(self._value)
1029 def __getattr__(self, attr):
1030 ''' no extended attribute accesses make sense here '''
1031 raise AttributeError, attr
1033 def __getitem__(self, num):
1034 ''' iterate and return a new HTMLItem
1035 '''
1036 #print 'Multi.getitem', (self, num)
1037 value = self._value[num]
1038 if self._prop.classname == 'user':
1039 klass = HTMLUser
1040 else:
1041 klass = HTMLItem
1042 return klass(self._client, self._prop.classname, value)
1044 def __contains__(self, value):
1045 ''' Support the "in" operator
1046 '''
1047 return value in self._value
1049 def reverse(self):
1050 ''' return the list in reverse order
1051 '''
1052 l = self._value[:]
1053 l.reverse()
1054 if self._prop.classname == 'user':
1055 klass = HTMLUser
1056 else:
1057 klass = HTMLItem
1058 return [klass(self._client, self._prop.classname, value) for value in l]
1060 def plain(self, escape=0):
1061 ''' Render a "plain" representation of the property
1062 '''
1063 linkcl = self._db.classes[self._prop.classname]
1064 k = linkcl.labelprop(1)
1065 labels = []
1066 for v in self._value:
1067 labels.append(linkcl.get(v, k))
1068 value = ', '.join(labels)
1069 if escape:
1070 value = cgi.escape(value)
1071 return value
1073 def field(self, size=30, showid=0):
1074 ''' Render a form edit field for the property
1075 '''
1076 sortfunc = make_sort_function(self._db, self._prop.classname)
1077 linkcl = self._db.getclass(self._prop.classname)
1078 value = self._value[:]
1079 if value:
1080 value.sort(sortfunc)
1081 # map the id to the label property
1082 if not linkcl.getkey():
1083 showid=1
1084 if not showid:
1085 k = linkcl.labelprop(1)
1086 value = [linkcl.get(v, k) for v in value]
1087 value = cgi.escape(','.join(value))
1088 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1090 def menu(self, size=None, height=None, showid=0, additional=[],
1091 **conditions):
1092 ''' Render a form select list for this property
1093 '''
1094 value = self._value
1096 # sort function
1097 sortfunc = make_sort_function(self._db, self._prop.classname)
1099 linkcl = self._db.getclass(self._prop.classname)
1100 if linkcl.getprops().has_key('order'):
1101 sort_on = ('+', 'order')
1102 else:
1103 sort_on = ('+', linkcl.labelprop())
1104 options = linkcl.filter(None, conditions, sort_on, (None,None))
1105 height = height or min(len(options), 7)
1106 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1107 k = linkcl.labelprop(1)
1108 for optionid in options:
1109 # get the option value, and if it's None use an empty string
1110 option = linkcl.get(optionid, k) or ''
1112 # figure if this option is selected
1113 s = ''
1114 if optionid in value or option in value:
1115 s = 'selected '
1117 # figure the label
1118 if showid:
1119 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1120 else:
1121 lab = option
1122 # truncate if it's too long
1123 if size is not None and len(lab) > size:
1124 lab = lab[:size-3] + '...'
1125 if additional:
1126 m = []
1127 for propname in additional:
1128 m.append(linkcl.get(optionid, propname))
1129 lab = lab + ' (%s)'%', '.join(m)
1131 # and generate
1132 lab = cgi.escape(lab)
1133 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1134 lab))
1135 l.append('</select>')
1136 return '\n'.join(l)
1138 # set the propclasses for HTMLItem
1139 propclasses = (
1140 (hyperdb.String, StringHTMLProperty),
1141 (hyperdb.Number, NumberHTMLProperty),
1142 (hyperdb.Boolean, BooleanHTMLProperty),
1143 (hyperdb.Date, DateHTMLProperty),
1144 (hyperdb.Interval, IntervalHTMLProperty),
1145 (hyperdb.Password, PasswordHTMLProperty),
1146 (hyperdb.Link, LinkHTMLProperty),
1147 (hyperdb.Multilink, MultilinkHTMLProperty),
1148 )
1150 def make_sort_function(db, classname):
1151 '''Make a sort function for a given class
1152 '''
1153 linkcl = db.getclass(classname)
1154 if linkcl.getprops().has_key('order'):
1155 sort_on = 'order'
1156 else:
1157 sort_on = linkcl.labelprop()
1158 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1159 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1160 return sortfunc
1162 def handleListCGIValue(value):
1163 ''' Value is either a single item or a list of items. Each item has a
1164 .value that we're actually interested in.
1165 '''
1166 if isinstance(value, type([])):
1167 return [value.value for value in value]
1168 else:
1169 value = value.value.strip()
1170 if not value:
1171 return []
1172 return value.split(',')
1174 class ShowDict:
1175 ''' A convenience access to the :columns index parameters
1176 '''
1177 def __init__(self, columns):
1178 self.columns = {}
1179 for col in columns:
1180 self.columns[col] = 1
1181 def __getitem__(self, name):
1182 return self.columns.has_key(name)
1184 class HTMLRequest:
1185 ''' The *request*, holding the CGI form and environment.
1187 "form" the CGI form as a cgi.FieldStorage
1188 "env" the CGI environment variables
1189 "base" the base URL for this instance
1190 "user" a HTMLUser instance for this user
1191 "classname" the current classname (possibly None)
1192 "template" the current template (suffix, also possibly None)
1194 Index args:
1195 "columns" dictionary of the columns to display in an index page
1196 "show" a convenience access to columns - request/show/colname will
1197 be true if the columns should be displayed, false otherwise
1198 "sort" index sort column (direction, column name)
1199 "group" index grouping property (direction, column name)
1200 "filter" properties to filter the index on
1201 "filterspec" values to filter the index on
1202 "search_text" text to perform a full-text search on for an index
1204 '''
1205 def __init__(self, client):
1206 self.client = client
1208 # easier access vars
1209 self.form = client.form
1210 self.env = client.env
1211 self.base = client.base
1212 self.user = HTMLUser(client, 'user', client.userid)
1214 # store the current class name and action
1215 self.classname = client.classname
1216 self.template = client.template
1218 self._post_init()
1220 def _post_init(self):
1221 ''' Set attributes based on self.form
1222 '''
1223 # extract the index display information from the form
1224 self.columns = []
1225 if self.form.has_key(':columns'):
1226 self.columns = handleListCGIValue(self.form[':columns'])
1227 self.show = ShowDict(self.columns)
1229 # sorting
1230 self.sort = (None, None)
1231 if self.form.has_key(':sort'):
1232 sort = self.form[':sort'].value
1233 if sort.startswith('-'):
1234 self.sort = ('-', sort[1:])
1235 else:
1236 self.sort = ('+', sort)
1237 if self.form.has_key(':sortdir'):
1238 self.sort = ('-', self.sort[1])
1240 # grouping
1241 self.group = (None, None)
1242 if self.form.has_key(':group'):
1243 group = self.form[':group'].value
1244 if group.startswith('-'):
1245 self.group = ('-', group[1:])
1246 else:
1247 self.group = ('+', group)
1248 if self.form.has_key(':groupdir'):
1249 self.group = ('-', self.group[1])
1251 # filtering
1252 self.filter = []
1253 if self.form.has_key(':filter'):
1254 self.filter = handleListCGIValue(self.form[':filter'])
1255 self.filterspec = {}
1256 db = self.client.db
1257 if self.classname is not None:
1258 props = db.getclass(self.classname).getprops()
1259 for name in self.filter:
1260 if self.form.has_key(name):
1261 prop = props[name]
1262 fv = self.form[name]
1263 if (isinstance(prop, hyperdb.Link) or
1264 isinstance(prop, hyperdb.Multilink)):
1265 self.filterspec[name] = lookupIds(db, prop,
1266 handleListCGIValue(fv))
1267 else:
1268 self.filterspec[name] = fv.value
1270 # full-text search argument
1271 self.search_text = None
1272 if self.form.has_key(':search_text'):
1273 self.search_text = self.form[':search_text'].value
1275 # pagination - size and start index
1276 # figure batch args
1277 if self.form.has_key(':pagesize'):
1278 self.pagesize = int(self.form[':pagesize'].value)
1279 else:
1280 self.pagesize = 50
1281 if self.form.has_key(':startwith'):
1282 self.startwith = int(self.form[':startwith'].value)
1283 else:
1284 self.startwith = 0
1286 def updateFromURL(self, url):
1287 ''' Parse the URL for query args, and update my attributes using the
1288 values.
1289 '''
1290 self.form = {}
1291 for name, value in cgi.parse_qsl(url):
1292 if self.form.has_key(name):
1293 if isinstance(self.form[name], type([])):
1294 self.form[name].append(cgi.MiniFieldStorage(name, value))
1295 else:
1296 self.form[name] = [self.form[name],
1297 cgi.MiniFieldStorage(name, value)]
1298 else:
1299 self.form[name] = cgi.MiniFieldStorage(name, value)
1300 self._post_init()
1302 def update(self, kwargs):
1303 ''' Update my attributes using the keyword args
1304 '''
1305 self.__dict__.update(kwargs)
1306 if kwargs.has_key('columns'):
1307 self.show = ShowDict(self.columns)
1309 def description(self):
1310 ''' Return a description of the request - handle for the page title.
1311 '''
1312 s = [self.client.db.config.TRACKER_NAME]
1313 if self.classname:
1314 if self.client.nodeid:
1315 s.append('- %s%s'%(self.classname, self.client.nodeid))
1316 else:
1317 if self.template == 'item':
1318 s.append('- new %s'%self.classname)
1319 elif self.template == 'index':
1320 s.append('- %s index'%self.classname)
1321 else:
1322 s.append('- %s %s'%(self.classname, self.template))
1323 else:
1324 s.append('- home')
1325 return ' '.join(s)
1327 def __str__(self):
1328 d = {}
1329 d.update(self.__dict__)
1330 f = ''
1331 for k in self.form.keys():
1332 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1333 d['form'] = f
1334 e = ''
1335 for k,v in self.env.items():
1336 e += '\n %r=%r'%(k, v)
1337 d['env'] = e
1338 return '''
1339 form: %(form)s
1340 base: %(base)r
1341 classname: %(classname)r
1342 template: %(template)r
1343 columns: %(columns)r
1344 sort: %(sort)r
1345 group: %(group)r
1346 filter: %(filter)r
1347 search_text: %(search_text)r
1348 pagesize: %(pagesize)r
1349 startwith: %(startwith)r
1350 env: %(env)s
1351 '''%d
1353 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1354 filterspec=1):
1355 ''' return the current index args as form elements '''
1356 l = []
1357 s = '<input type="hidden" name="%s" value="%s">'
1358 if columns and self.columns:
1359 l.append(s%(':columns', ','.join(self.columns)))
1360 if sort and self.sort[1] is not None:
1361 if self.sort[0] == '-':
1362 val = '-'+self.sort[1]
1363 else:
1364 val = self.sort[1]
1365 l.append(s%(':sort', val))
1366 if group and self.group[1] is not None:
1367 if self.group[0] == '-':
1368 val = '-'+self.group[1]
1369 else:
1370 val = self.group[1]
1371 l.append(s%(':group', val))
1372 if filter and self.filter:
1373 l.append(s%(':filter', ','.join(self.filter)))
1374 if filterspec:
1375 for k,v in self.filterspec.items():
1376 l.append(s%(k, ','.join(v)))
1377 if self.search_text:
1378 l.append(s%(':search_text', self.search_text))
1379 l.append(s%(':pagesize', self.pagesize))
1380 l.append(s%(':startwith', self.startwith))
1381 return '\n'.join(l)
1383 def indexargs_url(self, url, args):
1384 ''' embed the current index args in a URL '''
1385 l = ['%s=%s'%(k,v) for k,v in args.items()]
1386 if self.columns and not args.has_key(':columns'):
1387 l.append(':columns=%s'%(','.join(self.columns)))
1388 if self.sort[1] is not None and not args.has_key(':sort'):
1389 if self.sort[0] == '-':
1390 val = '-'+self.sort[1]
1391 else:
1392 val = self.sort[1]
1393 l.append(':sort=%s'%val)
1394 if self.group[1] is not None and not args.has_key(':group'):
1395 if self.group[0] == '-':
1396 val = '-'+self.group[1]
1397 else:
1398 val = self.group[1]
1399 l.append(':group=%s'%val)
1400 if self.filter and not args.has_key(':columns'):
1401 l.append(':filter=%s'%(','.join(self.filter)))
1402 for k,v in self.filterspec.items():
1403 if not args.has_key(k):
1404 l.append('%s=%s'%(k, ','.join(v)))
1405 if self.search_text and not args.has_key(':search_text'):
1406 l.append(':search_text=%s'%self.search_text)
1407 if not args.has_key(':pagesize'):
1408 l.append(':pagesize=%s'%self.pagesize)
1409 if not args.has_key(':startwith'):
1410 l.append(':startwith=%s'%self.startwith)
1411 return '%s?%s'%(url, '&'.join(l))
1412 indexargs_href = indexargs_url
1414 def base_javascript(self):
1415 return '''
1416 <script language="javascript">
1417 submitted = false;
1418 function submit_once() {
1419 if (submitted) {
1420 alert("Your request is being processed.\\nPlease be patient.");
1421 return 0;
1422 }
1423 submitted = true;
1424 return 1;
1425 }
1427 function help_window(helpurl, width, height) {
1428 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1429 }
1430 </script>
1431 '''%self.base
1433 def batch(self):
1434 ''' Return a batch object for results from the "current search"
1435 '''
1436 filterspec = self.filterspec
1437 sort = self.sort
1438 group = self.group
1440 # get the list of ids we're batching over
1441 klass = self.client.db.getclass(self.classname)
1442 if self.search_text:
1443 matches = self.client.db.indexer.search(
1444 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1445 else:
1446 matches = None
1447 l = klass.filter(matches, filterspec, sort, group)
1449 # return the batch object, using IDs only
1450 return Batch(self.client, l, self.pagesize, self.startwith,
1451 classname=self.classname)
1453 # extend the standard ZTUtils Batch object to remove dependency on
1454 # Acquisition and add a couple of useful methods
1455 class Batch(ZTUtils.Batch):
1456 ''' Use me to turn a list of items, or item ids of a given class, into a
1457 series of batches.
1459 ========= ========================================================
1460 Parameter Usage
1461 ========= ========================================================
1462 sequence a list of HTMLItems or item ids
1463 classname if sequence is a list of ids, this is the class of item
1464 size how big to make the sequence.
1465 start where to start (0-indexed) in the sequence.
1466 end where to end (0-indexed) in the sequence.
1467 orphan if the next batch would contain less items than this
1468 value, then it is combined with this batch
1469 overlap the number of items shared between adjacent batches
1470 ========= ========================================================
1472 Attributes: Note that the "start" attribute, unlike the
1473 argument, is a 1-based index (I know, lame). "first" is the
1474 0-based index. "length" is the actual number of elements in
1475 the batch.
1477 "sequence_length" is the length of the original, unbatched, sequence.
1478 '''
1479 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1480 overlap=0, classname=None):
1481 self.client = client
1482 self.last_index = self.last_item = None
1483 self.current_item = None
1484 self.classname = classname
1485 self.sequence_length = len(sequence)
1486 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1487 overlap)
1489 # overwrite so we can late-instantiate the HTMLItem instance
1490 def __getitem__(self, index):
1491 if index < 0:
1492 if index + self.end < self.first: raise IndexError, index
1493 return self._sequence[index + self.end]
1495 if index >= self.length:
1496 raise IndexError, index
1498 # move the last_item along - but only if the fetched index changes
1499 # (for some reason, index 0 is fetched twice)
1500 if index != self.last_index:
1501 self.last_item = self.current_item
1502 self.last_index = index
1504 item = self._sequence[index + self.first]
1505 if self.classname:
1506 # map the item ids to instances
1507 if self.classname == 'user':
1508 item = HTMLUser(self.client, self.classname, item)
1509 else:
1510 item = HTMLItem(self.client, self.classname, item)
1511 self.current_item = item
1512 return item
1514 def propchanged(self, property):
1515 ''' Detect if the property marked as being the group property
1516 changed in the last iteration fetch
1517 '''
1518 if (self.last_item is None or
1519 self.last_item[property] != self.current_item[property]):
1520 return 1
1521 return 0
1523 # override these 'cos we don't have access to acquisition
1524 def previous(self):
1525 if self.start == 1:
1526 return None
1527 return Batch(self.client, self._sequence, self._size,
1528 self.first - self._size + self.overlap, 0, self.orphan,
1529 self.overlap)
1531 def next(self):
1532 try:
1533 self._sequence[self.end]
1534 except IndexError:
1535 return None
1536 return Batch(self.client, self._sequence, self._size,
1537 self.end - self.overlap, 0, self.orphan, self.overlap)
1539 class TemplatingUtils:
1540 ''' Utilities for templating
1541 '''
1542 def __init__(self, client):
1543 self.client = client
1544 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1545 return Batch(self.client, sequence, size, start, end, orphan,
1546 overlap)