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