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
326 l = [klass(self._client, self.classname, x) for x in self._klass.list()]
327 return l
329 def csv(self):
330 ''' Return the items of this class as a chunk of CSV text.
331 '''
332 # get the CSV module
333 try:
334 import csv
335 except ImportError:
336 return 'Sorry, you need the csv module to use this function.\n'\
337 'Get it from: http://www.object-craft.com.au/projects/csv/'
339 props = self.propnames()
340 p = csv.parser()
341 s = StringIO.StringIO()
342 s.write(p.join(props) + '\n')
343 for nodeid in self._klass.list():
344 l = []
345 for name in props:
346 value = self._klass.get(nodeid, name)
347 if value is None:
348 l.append('')
349 elif isinstance(value, type([])):
350 l.append(':'.join(map(str, value)))
351 else:
352 l.append(str(self._klass.get(nodeid, name)))
353 s.write(p.join(l) + '\n')
354 return s.getvalue()
356 def propnames(self):
357 ''' Return the list of the names of the properties of this class.
358 '''
359 idlessprops = self._klass.getprops(protected=0).keys()
360 idlessprops.sort()
361 return ['id'] + idlessprops
363 def filter(self, request=None):
364 ''' Return a list of items from this class, filtered and sorted
365 by the current requested filterspec/filter/sort/group args
366 '''
367 if request is not None:
368 filterspec = request.filterspec
369 sort = request.sort
370 group = request.group
371 if self.classname == 'user':
372 klass = HTMLUser
373 else:
374 klass = HTMLItem
375 l = [klass(self._client, self.classname, x)
376 for x in self._klass.filter(None, filterspec, sort, group)]
377 return l
379 def classhelp(self, properties=None, label='list', width='500',
380 height='400'):
381 ''' Pop up a javascript window with class help
383 This generates a link to a popup window which displays the
384 properties indicated by "properties" of the class named by
385 "classname". The "properties" should be a comma-separated list
386 (eg. 'id,name,description'). Properties defaults to all the
387 properties of a class (excluding id, creator, created and
388 activity).
390 You may optionally override the label displayed, the width and
391 height. The popup window will be resizable and scrollable.
392 '''
393 if properties is None:
394 properties = self._klass.getprops(protected=0).keys()
395 properties.sort()
396 properties = ','.join(properties)
397 return '<a href="javascript:help_window(\'%s?:template=help&' \
398 ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
399 '(%s)</b></a>'%(self.classname, properties, width, height, label)
401 def submit(self, label="Submit New Entry"):
402 ''' Generate a submit button (and action hidden element)
403 '''
404 return ' <input type="hidden" name=":action" value="new">\n'\
405 ' <input type="submit" name="submit" value="%s">'%label
407 def history(self):
408 return 'New node - no history'
410 def renderWith(self, name, **kwargs):
411 ''' Render this class with the given template.
412 '''
413 # create a new request and override the specified args
414 req = HTMLRequest(self._client)
415 req.classname = self.classname
416 req.update(kwargs)
418 # new template, using the specified classname and request
419 pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
421 # use our fabricated request
422 return pt.render(self._client, self.classname, req)
424 class HTMLItem(HTMLPermissions):
425 ''' Accesses through an *item*
426 '''
427 def __init__(self, client, classname, nodeid):
428 self._client = client
429 self._db = client.db
430 self._classname = classname
431 self._nodeid = nodeid
432 self._klass = self._db.getclass(classname)
433 self._props = self._klass.getprops()
435 def __repr__(self):
436 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
437 self._nodeid)
439 def __getitem__(self, item):
440 ''' return an HTMLProperty instance
441 '''
442 #print 'HTMLItem.getitem', (self, item)
443 if item == 'id':
444 return self._nodeid
446 # get the property
447 prop = self._props[item]
449 # get the value, handling missing values
450 value = self._klass.get(self._nodeid, item, None)
451 if value is None:
452 if isinstance(self._props[item], hyperdb.Multilink):
453 value = []
455 # look up the correct HTMLProperty class
456 for klass, htmlklass in propclasses:
457 if isinstance(prop, klass):
458 return htmlklass(self._client, self._nodeid, prop, item, value)
460 raise KeyErorr, item
462 def __getattr__(self, attr):
463 ''' convenience access to properties '''
464 try:
465 return self[attr]
466 except KeyError:
467 raise AttributeError, attr
469 def submit(self, label="Submit Changes"):
470 ''' Generate a submit button (and action hidden element)
471 '''
472 return ' <input type="hidden" name=":action" value="edit">\n'\
473 ' <input type="submit" name="submit" value="%s">'%label
475 def journal(self, direction='descending'):
476 ''' Return a list of HTMLJournalEntry instances.
477 '''
478 # XXX do this
479 return []
481 def history(self, direction='descending'):
482 l = ['<table class="history">'
483 '<tr><th colspan="4" class="header">',
484 _('History'),
485 '</th></tr><tr>',
486 _('<th>Date</th>'),
487 _('<th>User</th>'),
488 _('<th>Action</th>'),
489 _('<th>Args</th>'),
490 '</tr>']
491 comments = {}
492 history = self._klass.history(self._nodeid)
493 history.sort()
494 if direction == 'descending':
495 history.reverse()
496 for id, evt_date, user, action, args in history:
497 date_s = str(evt_date).replace("."," ")
498 arg_s = ''
499 if action == 'link' and type(args) == type(()):
500 if len(args) == 3:
501 linkcl, linkid, key = args
502 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
503 linkcl, linkid, key)
504 else:
505 arg_s = str(args)
507 elif action == 'unlink' and type(args) == type(()):
508 if len(args) == 3:
509 linkcl, linkid, key = args
510 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
511 linkcl, linkid, key)
512 else:
513 arg_s = str(args)
515 elif type(args) == type({}):
516 cell = []
517 for k in args.keys():
518 # try to get the relevant property and treat it
519 # specially
520 try:
521 prop = self._props[k]
522 except KeyError:
523 prop = None
524 if prop is not None:
525 if args[k] and (isinstance(prop, hyperdb.Multilink) or
526 isinstance(prop, hyperdb.Link)):
527 # figure what the link class is
528 classname = prop.classname
529 try:
530 linkcl = self._db.getclass(classname)
531 except KeyError:
532 labelprop = None
533 comments[classname] = _('''The linked class
534 %(classname)s no longer exists''')%locals()
535 labelprop = linkcl.labelprop(1)
536 hrefable = os.path.exists(
537 os.path.join(self._db.config.TEMPLATES,
538 classname+'.item'))
540 if isinstance(prop, hyperdb.Multilink) and \
541 len(args[k]) > 0:
542 ml = []
543 for linkid in args[k]:
544 if isinstance(linkid, type(())):
545 sublabel = linkid[0] + ' '
546 linkids = linkid[1]
547 else:
548 sublabel = ''
549 linkids = [linkid]
550 subml = []
551 for linkid in linkids:
552 label = classname + linkid
553 # if we have a label property, try to use it
554 # TODO: test for node existence even when
555 # there's no labelprop!
556 try:
557 if labelprop is not None:
558 label = linkcl.get(linkid, labelprop)
559 except IndexError:
560 comments['no_link'] = _('''<strike>The
561 linked node no longer
562 exists</strike>''')
563 subml.append('<strike>%s</strike>'%label)
564 else:
565 if hrefable:
566 subml.append('<a href="%s%s">%s</a>'%(
567 classname, linkid, label))
568 ml.append(sublabel + ', '.join(subml))
569 cell.append('%s:\n %s'%(k, ', '.join(ml)))
570 elif isinstance(prop, hyperdb.Link) and args[k]:
571 label = classname + args[k]
572 # if we have a label property, try to use it
573 # TODO: test for node existence even when
574 # there's no labelprop!
575 if labelprop is not None:
576 try:
577 label = linkcl.get(args[k], labelprop)
578 except IndexError:
579 comments['no_link'] = _('''<strike>The
580 linked node no longer
581 exists</strike>''')
582 cell.append(' <strike>%s</strike>,\n'%label)
583 # "flag" this is done .... euwww
584 label = None
585 if label is not None:
586 if hrefable:
587 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
588 classname, args[k], label))
589 else:
590 cell.append('%s: %s' % (k,label))
592 elif isinstance(prop, hyperdb.Date) and args[k]:
593 d = date.Date(args[k])
594 cell.append('%s: %s'%(k, str(d)))
596 elif isinstance(prop, hyperdb.Interval) and args[k]:
597 d = date.Interval(args[k])
598 cell.append('%s: %s'%(k, str(d)))
600 elif isinstance(prop, hyperdb.String) and args[k]:
601 cell.append('%s: %s'%(k, cgi.escape(args[k])))
603 elif not args[k]:
604 cell.append('%s: (no value)\n'%k)
606 else:
607 cell.append('%s: %s\n'%(k, str(args[k])))
608 else:
609 # property no longer exists
610 comments['no_exist'] = _('''<em>The indicated property
611 no longer exists</em>''')
612 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
613 arg_s = '<br />'.join(cell)
614 else:
615 # unkown event!!
616 comments['unknown'] = _('''<strong><em>This event is not
617 handled by the history display!</em></strong>''')
618 arg_s = '<strong><em>' + str(args) + '</em></strong>'
619 date_s = date_s.replace(' ', ' ')
620 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
621 date_s, user, action, arg_s))
622 if comments:
623 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
624 for entry in comments.values():
625 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
626 l.append('</table>')
627 return '\n'.join(l)
629 def renderQueryForm(self):
630 ''' Render this item, which is a query, as a search form.
631 '''
632 # create a new request and override the specified args
633 req = HTMLRequest(self._client)
634 req.classname = self._klass.get(self._nodeid, 'klass')
635 req.updateFromURL(self._klass.get(self._nodeid, 'url'))
637 # new template, using the specified classname and request
638 pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
640 # use our fabricated request
641 return pt.render(self._client, req.classname, req)
643 class HTMLUser(HTMLItem):
644 ''' Accesses through the *user* (a special case of item)
645 '''
646 def __init__(self, client, classname, nodeid):
647 HTMLItem.__init__(self, client, 'user', nodeid)
648 self._default_classname = client.classname
650 # used for security checks
651 self._security = client.db.security
653 _marker = []
654 def hasPermission(self, role, classname=_marker):
655 ''' Determine if the user has the Role.
657 The class being tested defaults to the template's class, but may
658 be overidden for this test by suppling an alternate classname.
659 '''
660 if classname is self._marker:
661 classname = self._default_classname
662 return self._security.hasPermission(role, self._nodeid, classname)
664 def is_edit_ok(self):
665 ''' Is the user allowed to Edit the current class?
666 Also check whether this is the current user's info.
667 '''
668 return self._db.security.hasPermission('Edit', self._client.userid,
669 self._classname) or self._nodeid == self._client.userid
671 def is_view_ok(self):
672 ''' Is the user allowed to View the current class?
673 Also check whether this is the current user's info.
674 '''
675 return self._db.security.hasPermission('Edit', self._client.userid,
676 self._classname) or self._nodeid == self._client.userid
678 class HTMLProperty:
679 ''' String, Number, Date, Interval HTMLProperty
681 Has useful attributes:
683 _name the name of the property
684 _value the value of the property if any
686 A wrapper object which may be stringified for the plain() behaviour.
687 '''
688 def __init__(self, client, nodeid, prop, name, value):
689 self._client = client
690 self._db = client.db
691 self._nodeid = nodeid
692 self._prop = prop
693 self._name = name
694 self._value = value
695 def __repr__(self):
696 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
697 def __str__(self):
698 return self.plain()
699 def __cmp__(self, other):
700 if isinstance(other, HTMLProperty):
701 return cmp(self._value, other._value)
702 return cmp(self._value, other)
704 class StringHTMLProperty(HTMLProperty):
705 def plain(self, escape=0):
706 ''' Render a "plain" representation of the property
707 '''
708 if self._value is None:
709 return ''
710 if escape:
711 return cgi.escape(str(self._value))
712 return str(self._value)
714 def stext(self, escape=0):
715 ''' Render the value of the property as StructuredText.
717 This requires the StructureText module to be installed separately.
718 '''
719 s = self.plain(escape=escape)
720 if not StructuredText:
721 return s
722 return StructuredText(s,level=1,header=0)
724 def field(self, size = 30):
725 ''' Render a form edit field for the property
726 '''
727 if self._value is None:
728 value = ''
729 else:
730 value = cgi.escape(str(self._value))
731 value = '"'.join(value.split('"'))
732 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
734 def multiline(self, escape=0, rows=5, cols=40):
735 ''' Render a multiline form edit field for the property
736 '''
737 if self._value is None:
738 value = ''
739 else:
740 value = cgi.escape(str(self._value))
741 value = '"'.join(value.split('"'))
742 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
743 self._name, rows, cols, value)
745 def email(self, escape=1):
746 ''' Render the value of the property as an obscured email address
747 '''
748 if self._value is None: value = ''
749 else: value = str(self._value)
750 if value.find('@') != -1:
751 name, domain = value.split('@')
752 domain = ' '.join(domain.split('.')[:-1])
753 name = name.replace('.', ' ')
754 value = '%s at %s ...'%(name, domain)
755 else:
756 value = value.replace('.', ' ')
757 if escape:
758 value = cgi.escape(value)
759 return value
761 class PasswordHTMLProperty(HTMLProperty):
762 def plain(self):
763 ''' Render a "plain" representation of the property
764 '''
765 if self._value is None:
766 return ''
767 return _('*encrypted*')
769 def field(self, size = 30):
770 ''' Render a form edit field for the property.
771 '''
772 return '<input type="password" name="%s" size="%s">'%(self._name, size)
774 def confirm(self, size = 30):
775 ''' Render a second form edit field for the property, used for
776 confirmation that the user typed the password correctly. Generates
777 a field with name "name:confirm".
778 '''
779 return '<input type="password" name="%s:confirm" size="%s">'%(
780 self._name, size)
782 class NumberHTMLProperty(HTMLProperty):
783 def plain(self):
784 ''' Render a "plain" representation of the property
785 '''
786 return str(self._value)
788 def field(self, size = 30):
789 ''' Render a form edit field for the property
790 '''
791 if self._value is None:
792 value = ''
793 else:
794 value = cgi.escape(str(self._value))
795 value = '"'.join(value.split('"'))
796 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
798 class BooleanHTMLProperty(HTMLProperty):
799 def plain(self):
800 ''' Render a "plain" representation of the property
801 '''
802 if self.value is None:
803 return ''
804 return self._value and "Yes" or "No"
806 def field(self):
807 ''' Render a form edit field for the property
808 '''
809 checked = self._value and "checked" or ""
810 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
811 checked)
812 if checked:
813 checked = ""
814 else:
815 checked = "checked"
816 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
817 checked)
818 return s
820 class DateHTMLProperty(HTMLProperty):
821 def plain(self):
822 ''' Render a "plain" representation of the property
823 '''
824 if self._value is None:
825 return ''
826 return str(self._value)
828 def field(self, size = 30):
829 ''' Render a form edit field for the property
830 '''
831 if self._value is None:
832 value = ''
833 else:
834 value = cgi.escape(str(self._value))
835 value = '"'.join(value.split('"'))
836 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
838 def reldate(self, pretty=1):
839 ''' Render the interval between the date and now.
841 If the "pretty" flag is true, then make the display pretty.
842 '''
843 if not self._value:
844 return ''
846 # figure the interval
847 interval = date.Date('.') - self._value
848 if pretty:
849 return interval.pretty()
850 return str(interval)
852 class IntervalHTMLProperty(HTMLProperty):
853 def plain(self):
854 ''' Render a "plain" representation of the property
855 '''
856 if self._value is None:
857 return ''
858 return str(self._value)
860 def pretty(self):
861 ''' Render the interval in a pretty format (eg. "yesterday")
862 '''
863 return self._value.pretty()
865 def field(self, size = 30):
866 ''' Render a form edit field for the property
867 '''
868 if self._value is None:
869 value = ''
870 else:
871 value = cgi.escape(str(self._value))
872 value = '"'.join(value.split('"'))
873 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
875 class LinkHTMLProperty(HTMLProperty):
876 ''' Link HTMLProperty
877 Include the above as well as being able to access the class
878 information. Stringifying the object itself results in the value
879 from the item being displayed. Accessing attributes of this object
880 result in the appropriate entry from the class being queried for the
881 property accessed (so item/assignedto/name would look up the user
882 entry identified by the assignedto property on item, and then the
883 name property of that user)
884 '''
885 def __getattr__(self, attr):
886 ''' return a new HTMLItem '''
887 #print 'Link.getattr', (self, attr, self._value)
888 if not self._value:
889 raise AttributeError, "Can't access missing value"
890 if self._prop.classname == 'user':
891 klass = HTMLUser
892 else:
893 klass = HTMLItem
894 i = klass(self._client, self._prop.classname, self._value)
895 return getattr(i, attr)
897 def plain(self, escape=0):
898 ''' Render a "plain" representation of the property
899 '''
900 if self._value is None:
901 return ''
902 linkcl = self._db.classes[self._prop.classname]
903 k = linkcl.labelprop(1)
904 value = str(linkcl.get(self._value, k))
905 if escape:
906 value = cgi.escape(value)
907 return value
909 def field(self, showid=0, size=None):
910 ''' Render a form edit field for the property
911 '''
912 linkcl = self._db.getclass(self._prop.classname)
913 if linkcl.getprops().has_key('order'):
914 sort_on = 'order'
915 else:
916 sort_on = linkcl.labelprop()
917 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
918 # TODO: make this a field display, not a menu one!
919 l = ['<select name="%s">'%self._name]
920 k = linkcl.labelprop(1)
921 if self._value is None:
922 s = 'selected '
923 else:
924 s = ''
925 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
926 for optionid in options:
927 option = linkcl.get(optionid, k)
928 s = ''
929 if optionid == self._value:
930 s = 'selected '
931 if showid:
932 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
933 else:
934 lab = option
935 if size is not None and len(lab) > size:
936 lab = lab[:size-3] + '...'
937 lab = cgi.escape(lab)
938 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
939 l.append('</select>')
940 return '\n'.join(l)
942 def menu(self, size=None, height=None, showid=0, additional=[],
943 **conditions):
944 ''' Render a form select list for this property
945 '''
946 value = self._value
948 # sort function
949 sortfunc = make_sort_function(self._db, self._prop.classname)
951 # force the value to be a single choice
952 if isinstance(value, type('')):
953 value = value[0]
954 linkcl = self._db.getclass(self._prop.classname)
955 l = ['<select name="%s">'%self._name]
956 k = linkcl.labelprop(1)
957 s = ''
958 if value is None:
959 s = 'selected '
960 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
961 if linkcl.getprops().has_key('order'):
962 sort_on = ('+', 'order')
963 else:
964 sort_on = ('+', linkcl.labelprop())
965 options = linkcl.filter(None, conditions, sort_on, (None, None))
966 for optionid in options:
967 option = linkcl.get(optionid, k)
968 s = ''
969 if value in [optionid, option]:
970 s = 'selected '
971 if showid:
972 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
973 else:
974 lab = option
975 if size is not None and len(lab) > size:
976 lab = lab[:size-3] + '...'
977 if additional:
978 m = []
979 for propname in additional:
980 m.append(linkcl.get(optionid, propname))
981 lab = lab + ' (%s)'%', '.join(map(str, m))
982 lab = cgi.escape(lab)
983 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
984 l.append('</select>')
985 return '\n'.join(l)
986 # def checklist(self, ...)
988 class MultilinkHTMLProperty(HTMLProperty):
989 ''' Multilink HTMLProperty
991 Also be iterable, returning a wrapper object like the Link case for
992 each entry in the multilink.
993 '''
994 def __len__(self):
995 ''' length of the multilink '''
996 return len(self._value)
998 def __getattr__(self, attr):
999 ''' no extended attribute accesses make sense here '''
1000 raise AttributeError, attr
1002 def __getitem__(self, num):
1003 ''' iterate and return a new HTMLItem
1004 '''
1005 #print 'Multi.getitem', (self, num)
1006 value = self._value[num]
1007 if self._prop.classname == 'user':
1008 klass = HTMLUser
1009 else:
1010 klass = HTMLItem
1011 return klass(self._client, self._prop.classname, value)
1013 def __contains__(self, value):
1014 ''' Support the "in" operator
1015 '''
1016 return value in self._value
1018 def reverse(self):
1019 ''' return the list in reverse order
1020 '''
1021 l = self._value[:]
1022 l.reverse()
1023 if self._prop.classname == 'user':
1024 klass = HTMLUser
1025 else:
1026 klass = HTMLItem
1027 return [klass(self._client, self._prop.classname, value) for value in l]
1029 def plain(self, escape=0):
1030 ''' Render a "plain" representation of the property
1031 '''
1032 linkcl = self._db.classes[self._prop.classname]
1033 k = linkcl.labelprop(1)
1034 labels = []
1035 for v in self._value:
1036 labels.append(linkcl.get(v, k))
1037 value = ', '.join(labels)
1038 if escape:
1039 value = cgi.escape(value)
1040 return value
1042 def field(self, size=30, showid=0):
1043 ''' Render a form edit field for the property
1044 '''
1045 sortfunc = make_sort_function(self._db, self._prop.classname)
1046 linkcl = self._db.getclass(self._prop.classname)
1047 value = self._value[:]
1048 if value:
1049 value.sort(sortfunc)
1050 # map the id to the label property
1051 if not linkcl.getkey():
1052 showid=1
1053 if not showid:
1054 k = linkcl.labelprop(1)
1055 value = [linkcl.get(v, k) for v in value]
1056 value = cgi.escape(','.join(value))
1057 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1059 def menu(self, size=None, height=None, showid=0, additional=[],
1060 **conditions):
1061 ''' Render a form select list for this property
1062 '''
1063 value = self._value
1065 # sort function
1066 sortfunc = make_sort_function(self._db, self._prop.classname)
1068 linkcl = self._db.getclass(self._prop.classname)
1069 if linkcl.getprops().has_key('order'):
1070 sort_on = ('+', 'order')
1071 else:
1072 sort_on = ('+', linkcl.labelprop())
1073 options = linkcl.filter(None, conditions, sort_on, (None,None))
1074 height = height or min(len(options), 7)
1075 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1076 k = linkcl.labelprop(1)
1077 for optionid in options:
1078 option = linkcl.get(optionid, k)
1079 s = ''
1080 if optionid in value or option in value:
1081 s = 'selected '
1082 if showid:
1083 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1084 else:
1085 lab = option
1086 if size is not None and len(lab) > size:
1087 lab = lab[:size-3] + '...'
1088 if additional:
1089 m = []
1090 for propname in additional:
1091 m.append(linkcl.get(optionid, propname))
1092 lab = lab + ' (%s)'%', '.join(m)
1093 lab = cgi.escape(lab)
1094 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1095 lab))
1096 l.append('</select>')
1097 return '\n'.join(l)
1099 # set the propclasses for HTMLItem
1100 propclasses = (
1101 (hyperdb.String, StringHTMLProperty),
1102 (hyperdb.Number, NumberHTMLProperty),
1103 (hyperdb.Boolean, BooleanHTMLProperty),
1104 (hyperdb.Date, DateHTMLProperty),
1105 (hyperdb.Interval, IntervalHTMLProperty),
1106 (hyperdb.Password, PasswordHTMLProperty),
1107 (hyperdb.Link, LinkHTMLProperty),
1108 (hyperdb.Multilink, MultilinkHTMLProperty),
1109 )
1111 def make_sort_function(db, classname):
1112 '''Make a sort function for a given class
1113 '''
1114 linkcl = db.getclass(classname)
1115 if linkcl.getprops().has_key('order'):
1116 sort_on = 'order'
1117 else:
1118 sort_on = linkcl.labelprop()
1119 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1120 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1121 return sortfunc
1123 def handleListCGIValue(value):
1124 ''' Value is either a single item or a list of items. Each item has a
1125 .value that we're actually interested in.
1126 '''
1127 if isinstance(value, type([])):
1128 return [value.value for value in value]
1129 else:
1130 value = value.value.strip()
1131 if not value:
1132 return []
1133 return value.split(',')
1135 class ShowDict:
1136 ''' A convenience access to the :columns index parameters
1137 '''
1138 def __init__(self, columns):
1139 self.columns = {}
1140 for col in columns:
1141 self.columns[col] = 1
1142 def __getitem__(self, name):
1143 return self.columns.has_key(name)
1145 class HTMLRequest:
1146 ''' The *request*, holding the CGI form and environment.
1148 "form" the CGI form as a cgi.FieldStorage
1149 "env" the CGI environment variables
1150 "url" the current URL path for this request
1151 "base" the base URL for this instance
1152 "user" a HTMLUser instance for this user
1153 "classname" the current classname (possibly None)
1154 "template" the current template (suffix, also possibly None)
1156 Index args:
1157 "columns" dictionary of the columns to display in an index page
1158 "show" a convenience access to columns - request/show/colname will
1159 be true if the columns should be displayed, false otherwise
1160 "sort" index sort column (direction, column name)
1161 "group" index grouping property (direction, column name)
1162 "filter" properties to filter the index on
1163 "filterspec" values to filter the index on
1164 "search_text" text to perform a full-text search on for an index
1166 '''
1167 def __init__(self, client):
1168 self.client = client
1170 # easier access vars
1171 self.form = client.form
1172 self.env = client.env
1173 self.base = client.base
1174 self.url = client.url
1175 self.user = HTMLUser(client, 'user', client.userid)
1177 # store the current class name and action
1178 self.classname = client.classname
1179 self.template = client.template
1181 self._post_init()
1183 def _post_init(self):
1184 ''' Set attributes based on self.form
1185 '''
1186 # extract the index display information from the form
1187 self.columns = []
1188 if self.form.has_key(':columns'):
1189 self.columns = handleListCGIValue(self.form[':columns'])
1190 self.show = ShowDict(self.columns)
1192 # sorting
1193 self.sort = (None, None)
1194 if self.form.has_key(':sort'):
1195 sort = self.form[':sort'].value
1196 if sort.startswith('-'):
1197 self.sort = ('-', sort[1:])
1198 else:
1199 self.sort = ('+', sort)
1200 if self.form.has_key(':sortdir'):
1201 self.sort = ('-', self.sort[1])
1203 # grouping
1204 self.group = (None, None)
1205 if self.form.has_key(':group'):
1206 group = self.form[':group'].value
1207 if group.startswith('-'):
1208 self.group = ('-', group[1:])
1209 else:
1210 self.group = ('+', group)
1211 if self.form.has_key(':groupdir'):
1212 self.group = ('-', self.group[1])
1214 # filtering
1215 self.filter = []
1216 if self.form.has_key(':filter'):
1217 self.filter = handleListCGIValue(self.form[':filter'])
1218 self.filterspec = {}
1219 if self.classname is not None:
1220 props = self.client.db.getclass(self.classname).getprops()
1221 for name in self.filter:
1222 if self.form.has_key(name):
1223 prop = props[name]
1224 fv = self.form[name]
1225 if (isinstance(prop, hyperdb.Link) or
1226 isinstance(prop, hyperdb.Multilink)):
1227 self.filterspec[name] = handleListCGIValue(fv)
1228 else:
1229 self.filterspec[name] = fv.value
1231 # full-text search argument
1232 self.search_text = None
1233 if self.form.has_key(':search_text'):
1234 self.search_text = self.form[':search_text'].value
1236 # pagination - size and start index
1237 # figure batch args
1238 if self.form.has_key(':pagesize'):
1239 self.pagesize = int(self.form[':pagesize'].value)
1240 else:
1241 self.pagesize = 50
1242 if self.form.has_key(':startwith'):
1243 self.startwith = int(self.form[':startwith'].value)
1244 else:
1245 self.startwith = 0
1247 def updateFromURL(self, url):
1248 ''' Parse the URL for query args, and update my attributes using the
1249 values.
1250 '''
1251 self.form = {}
1252 for name, value in cgi.parse_qsl(url):
1253 if self.form.has_key(name):
1254 if isinstance(self.form[name], type([])):
1255 self.form[name].append(cgi.MiniFieldStorage(name, value))
1256 else:
1257 self.form[name] = [self.form[name],
1258 cgi.MiniFieldStorage(name, value)]
1259 else:
1260 self.form[name] = cgi.MiniFieldStorage(name, value)
1261 self._post_init()
1263 def update(self, kwargs):
1264 ''' Update my attributes using the keyword args
1265 '''
1266 self.__dict__.update(kwargs)
1267 if kwargs.has_key('columns'):
1268 self.show = ShowDict(self.columns)
1270 def description(self):
1271 ''' Return a description of the request - handle for the page title.
1272 '''
1273 s = [self.client.db.config.TRACKER_NAME]
1274 if self.classname:
1275 if self.client.nodeid:
1276 s.append('- %s%s'%(self.classname, self.client.nodeid))
1277 else:
1278 if self.template == 'item':
1279 s.append('- new %s'%self.classname)
1280 elif self.template == 'index':
1281 s.append('- %s index'%self.classname)
1282 else:
1283 s.append('- %s %s'%(self.classname, self.template))
1284 else:
1285 s.append('- home')
1286 return ' '.join(s)
1288 def __str__(self):
1289 d = {}
1290 d.update(self.__dict__)
1291 f = ''
1292 for k in self.form.keys():
1293 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1294 d['form'] = f
1295 e = ''
1296 for k,v in self.env.items():
1297 e += '\n %r=%r'%(k, v)
1298 d['env'] = e
1299 return '''
1300 form: %(form)s
1301 url: %(url)r
1302 base: %(base)r
1303 classname: %(classname)r
1304 template: %(template)r
1305 columns: %(columns)r
1306 sort: %(sort)r
1307 group: %(group)r
1308 filter: %(filter)r
1309 search_text: %(search_text)r
1310 pagesize: %(pagesize)r
1311 startwith: %(startwith)r
1312 env: %(env)s
1313 '''%d
1315 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1316 filterspec=1):
1317 ''' return the current index args as form elements '''
1318 l = []
1319 s = '<input type="hidden" name="%s" value="%s">'
1320 if columns and self.columns:
1321 l.append(s%(':columns', ','.join(self.columns)))
1322 if sort and self.sort[1] is not None:
1323 if self.sort[0] == '-':
1324 val = '-'+self.sort[1]
1325 else:
1326 val = self.sort[1]
1327 l.append(s%(':sort', val))
1328 if group and self.group[1] is not None:
1329 if self.group[0] == '-':
1330 val = '-'+self.group[1]
1331 else:
1332 val = self.group[1]
1333 l.append(s%(':group', val))
1334 if filter and self.filter:
1335 l.append(s%(':filter', ','.join(self.filter)))
1336 if filterspec:
1337 for k,v in self.filterspec.items():
1338 l.append(s%(k, ','.join(v)))
1339 if self.search_text:
1340 l.append(s%(':search_text', self.search_text))
1341 l.append(s%(':pagesize', self.pagesize))
1342 l.append(s%(':startwith', self.startwith))
1343 return '\n'.join(l)
1345 def indexargs_url(self, url, args):
1346 ''' embed the current index args in a URL '''
1347 l = ['%s=%s'%(k,v) for k,v in args.items()]
1348 if self.columns and not args.has_key(':columns'):
1349 l.append(':columns=%s'%(','.join(self.columns)))
1350 if self.sort[1] is not None and not args.has_key(':sort'):
1351 if self.sort[0] == '-':
1352 val = '-'+self.sort[1]
1353 else:
1354 val = self.sort[1]
1355 l.append(':sort=%s'%val)
1356 if self.group[1] is not None and not args.has_key(':group'):
1357 if self.group[0] == '-':
1358 val = '-'+self.group[1]
1359 else:
1360 val = self.group[1]
1361 l.append(':group=%s'%val)
1362 if self.filter and not args.has_key(':columns'):
1363 l.append(':filter=%s'%(','.join(self.filter)))
1364 for k,v in self.filterspec.items():
1365 if not args.has_key(k):
1366 l.append('%s=%s'%(k, ','.join(v)))
1367 if self.search_text and not args.has_key(':search_text'):
1368 l.append(':search_text=%s'%self.search_text)
1369 if not args.has_key(':pagesize'):
1370 l.append(':pagesize=%s'%self.pagesize)
1371 if not args.has_key(':startwith'):
1372 l.append(':startwith=%s'%self.startwith)
1373 return '%s?%s'%(url, '&'.join(l))
1374 indexargs_href = indexargs_url
1376 def base_javascript(self):
1377 return '''
1378 <script language="javascript">
1379 submitted = false;
1380 function submit_once() {
1381 if (submitted) {
1382 alert("Your request is being processed.\\nPlease be patient.");
1383 return 0;
1384 }
1385 submitted = true;
1386 return 1;
1387 }
1389 function help_window(helpurl, width, height) {
1390 HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1391 }
1392 </script>
1393 '''%self.base
1395 def batch(self):
1396 ''' Return a batch object for results from the "current search"
1397 '''
1398 filterspec = self.filterspec
1399 sort = self.sort
1400 group = self.group
1402 # get the list of ids we're batching over
1403 klass = self.client.db.getclass(self.classname)
1404 if self.search_text:
1405 matches = self.client.db.indexer.search(
1406 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1407 else:
1408 matches = None
1409 l = klass.filter(matches, filterspec, sort, group)
1411 # map the item ids to instances
1412 if self.classname == 'user':
1413 klass = HTMLUser
1414 else:
1415 klass = HTMLItem
1416 l = [klass(self.client, self.classname, item) for item in l]
1418 # return the batch object
1419 return Batch(self.client, l, self.pagesize, self.startwith)
1421 # extend the standard ZTUtils Batch object to remove dependency on
1422 # Acquisition and add a couple of useful methods
1423 class Batch(ZTUtils.Batch):
1424 ''' Use me to turn a list of items, or item ids of a given class, into a
1425 series of batches.
1427 ========= ========================================================
1428 Parameter Usage
1429 ========= ========================================================
1430 sequence a list of HTMLItems
1431 size how big to make the sequence.
1432 start where to start (0-indexed) in the sequence.
1433 end where to end (0-indexed) in the sequence.
1434 orphan if the next batch would contain less items than this
1435 value, then it is combined with this batch
1436 overlap the number of items shared between adjacent batches
1437 ========= ========================================================
1439 Attributes: Note that the "start" attribute, unlike the
1440 argument, is a 1-based index (I know, lame). "first" is the
1441 0-based index. "length" is the actual number of elements in
1442 the batch.
1444 "sequence_length" is the length of the original, unbatched, sequence.
1445 '''
1446 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1447 overlap=0):
1448 self.client = client
1449 self.last_index = self.last_item = None
1450 self.current_item = None
1451 self.sequence_length = len(sequence)
1452 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1453 overlap)
1455 # overwrite so we can late-instantiate the HTMLItem instance
1456 def __getitem__(self, index):
1457 if index < 0:
1458 if index + self.end < self.first: raise IndexError, index
1459 return self._sequence[index + self.end]
1461 if index >= self.length:
1462 raise IndexError, index
1464 # move the last_item along - but only if the fetched index changes
1465 # (for some reason, index 0 is fetched twice)
1466 if index != self.last_index:
1467 self.last_item = self.current_item
1468 self.last_index = index
1470 self.current_item = self._sequence[index + self.first]
1471 return self.current_item
1473 def propchanged(self, property):
1474 ''' Detect if the property marked as being the group property
1475 changed in the last iteration fetch
1476 '''
1477 if (self.last_item is None or
1478 self.last_item[property] != self.current_item[property]):
1479 return 1
1480 return 0
1482 # override these 'cos we don't have access to acquisition
1483 def previous(self):
1484 if self.start == 1:
1485 return None
1486 return Batch(self.client, self._sequence, self._size,
1487 self.first - self._size + self.overlap, 0, self.orphan,
1488 self.overlap)
1490 def next(self):
1491 try:
1492 self._sequence[self.end]
1493 except IndexError:
1494 return None
1495 return Batch(self.client, self._sequence, self._size,
1496 self.end - self.overlap, 0, self.orphan, self.overlap)
1498 class TemplatingUtils:
1499 ''' Utilities for templating
1500 '''
1501 def __init__(self, client):
1502 self.client = client
1503 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1504 return Batch(self.client, sequence, size, start, end, orphan,
1505 overlap)