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._prop.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 option = linkcl.get(optionid, k)
934 s = ''
935 if optionid == self._value:
936 s = 'selected '
937 if showid:
938 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
939 else:
940 lab = option
941 if size is not None and len(lab) > size:
942 lab = lab[:size-3] + '...'
943 lab = cgi.escape(lab)
944 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
945 l.append('</select>')
946 return '\n'.join(l)
948 def menu(self, size=None, height=None, showid=0, additional=[],
949 **conditions):
950 ''' Render a form select list for this property
951 '''
952 value = self._value
954 # sort function
955 sortfunc = make_sort_function(self._db, self._prop.classname)
957 linkcl = self._db.getclass(self._prop.classname)
958 l = ['<select name="%s">'%self._name]
959 k = linkcl.labelprop(1)
960 s = ''
961 if value is None:
962 s = 'selected '
963 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
964 if linkcl.getprops().has_key('order'):
965 sort_on = ('+', 'order')
966 else:
967 sort_on = ('+', linkcl.labelprop())
968 options = linkcl.filter(None, conditions, sort_on, (None, None))
969 for optionid in options:
970 option = linkcl.get(optionid, k)
971 s = ''
972 if value in [optionid, option]:
973 s = 'selected '
974 if showid:
975 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
976 else:
977 lab = option
978 if size is not None and len(lab) > size:
979 lab = lab[:size-3] + '...'
980 if additional:
981 m = []
982 for propname in additional:
983 m.append(linkcl.get(optionid, propname))
984 lab = lab + ' (%s)'%', '.join(map(str, m))
985 lab = cgi.escape(lab)
986 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
987 l.append('</select>')
988 return '\n'.join(l)
989 # def checklist(self, ...)
991 class MultilinkHTMLProperty(HTMLProperty):
992 ''' Multilink HTMLProperty
994 Also be iterable, returning a wrapper object like the Link case for
995 each entry in the multilink.
996 '''
997 def __len__(self):
998 ''' length of the multilink '''
999 return len(self._value)
1001 def __getattr__(self, attr):
1002 ''' no extended attribute accesses make sense here '''
1003 raise AttributeError, attr
1005 def __getitem__(self, num):
1006 ''' iterate and return a new HTMLItem
1007 '''
1008 #print 'Multi.getitem', (self, num)
1009 value = self._value[num]
1010 if self._prop.classname == 'user':
1011 klass = HTMLUser
1012 else:
1013 klass = HTMLItem
1014 return klass(self._client, self._prop.classname, value)
1016 def __contains__(self, value):
1017 ''' Support the "in" operator
1018 '''
1019 return value in self._value
1021 def reverse(self):
1022 ''' return the list in reverse order
1023 '''
1024 l = self._value[:]
1025 l.reverse()
1026 if self._prop.classname == 'user':
1027 klass = HTMLUser
1028 else:
1029 klass = HTMLItem
1030 return [klass(self._client, self._prop.classname, value) for value in l]
1032 def plain(self, escape=0):
1033 ''' Render a "plain" representation of the property
1034 '''
1035 linkcl = self._db.classes[self._prop.classname]
1036 k = linkcl.labelprop(1)
1037 labels = []
1038 for v in self._value:
1039 labels.append(linkcl.get(v, k))
1040 value = ', '.join(labels)
1041 if escape:
1042 value = cgi.escape(value)
1043 return value
1045 def field(self, size=30, showid=0):
1046 ''' Render a form edit field for the property
1047 '''
1048 sortfunc = make_sort_function(self._db, self._prop.classname)
1049 linkcl = self._db.getclass(self._prop.classname)
1050 value = self._value[:]
1051 if value:
1052 value.sort(sortfunc)
1053 # map the id to the label property
1054 if not linkcl.getkey():
1055 showid=1
1056 if not showid:
1057 k = linkcl.labelprop(1)
1058 value = [linkcl.get(v, k) for v in value]
1059 value = cgi.escape(','.join(value))
1060 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1062 def menu(self, size=None, height=None, showid=0, additional=[],
1063 **conditions):
1064 ''' Render a form select list for this property
1065 '''
1066 value = self._value
1068 # sort function
1069 sortfunc = make_sort_function(self._db, self._prop.classname)
1071 linkcl = self._db.getclass(self._prop.classname)
1072 if linkcl.getprops().has_key('order'):
1073 sort_on = ('+', 'order')
1074 else:
1075 sort_on = ('+', linkcl.labelprop())
1076 options = linkcl.filter(None, conditions, sort_on, (None,None))
1077 height = height or min(len(options), 7)
1078 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1079 k = linkcl.labelprop(1)
1080 for optionid in options:
1081 option = linkcl.get(optionid, k)
1082 s = ''
1083 if optionid in value or option in value:
1084 s = 'selected '
1085 if showid:
1086 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1087 else:
1088 lab = option
1089 if size is not None and len(lab) > size:
1090 lab = lab[:size-3] + '...'
1091 if additional:
1092 m = []
1093 for propname in additional:
1094 m.append(linkcl.get(optionid, propname))
1095 lab = lab + ' (%s)'%', '.join(m)
1096 lab = cgi.escape(lab)
1097 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1098 lab))
1099 l.append('</select>')
1100 return '\n'.join(l)
1102 # set the propclasses for HTMLItem
1103 propclasses = (
1104 (hyperdb.String, StringHTMLProperty),
1105 (hyperdb.Number, NumberHTMLProperty),
1106 (hyperdb.Boolean, BooleanHTMLProperty),
1107 (hyperdb.Date, DateHTMLProperty),
1108 (hyperdb.Interval, IntervalHTMLProperty),
1109 (hyperdb.Password, PasswordHTMLProperty),
1110 (hyperdb.Link, LinkHTMLProperty),
1111 (hyperdb.Multilink, MultilinkHTMLProperty),
1112 )
1114 def make_sort_function(db, classname):
1115 '''Make a sort function for a given class
1116 '''
1117 linkcl = db.getclass(classname)
1118 if linkcl.getprops().has_key('order'):
1119 sort_on = 'order'
1120 else:
1121 sort_on = linkcl.labelprop()
1122 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1123 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1124 return sortfunc
1126 def handleListCGIValue(value):
1127 ''' Value is either a single item or a list of items. Each item has a
1128 .value that we're actually interested in.
1129 '''
1130 if isinstance(value, type([])):
1131 return [value.value for value in value]
1132 else:
1133 value = value.value.strip()
1134 if not value:
1135 return []
1136 return value.split(',')
1138 class ShowDict:
1139 ''' A convenience access to the :columns index parameters
1140 '''
1141 def __init__(self, columns):
1142 self.columns = {}
1143 for col in columns:
1144 self.columns[col] = 1
1145 def __getitem__(self, name):
1146 return self.columns.has_key(name)
1148 class HTMLRequest:
1149 ''' The *request*, holding the CGI form and environment.
1151 "form" the CGI form as a cgi.FieldStorage
1152 "env" the CGI environment variables
1153 "base" the base URL for this instance
1154 "user" a HTMLUser instance for this user
1155 "classname" the current classname (possibly None)
1156 "template" the current template (suffix, also possibly None)
1158 Index args:
1159 "columns" dictionary of the columns to display in an index page
1160 "show" a convenience access to columns - request/show/colname will
1161 be true if the columns should be displayed, false otherwise
1162 "sort" index sort column (direction, column name)
1163 "group" index grouping property (direction, column name)
1164 "filter" properties to filter the index on
1165 "filterspec" values to filter the index on
1166 "search_text" text to perform a full-text search on for an index
1168 '''
1169 def __init__(self, client):
1170 self.client = client
1172 # easier access vars
1173 self.form = client.form
1174 self.env = client.env
1175 self.base = client.base
1176 self.user = HTMLUser(client, 'user', client.userid)
1178 # store the current class name and action
1179 self.classname = client.classname
1180 self.template = client.template
1182 self._post_init()
1184 def _post_init(self):
1185 ''' Set attributes based on self.form
1186 '''
1187 # extract the index display information from the form
1188 self.columns = []
1189 if self.form.has_key(':columns'):
1190 self.columns = handleListCGIValue(self.form[':columns'])
1191 self.show = ShowDict(self.columns)
1193 # sorting
1194 self.sort = (None, None)
1195 if self.form.has_key(':sort'):
1196 sort = self.form[':sort'].value
1197 if sort.startswith('-'):
1198 self.sort = ('-', sort[1:])
1199 else:
1200 self.sort = ('+', sort)
1201 if self.form.has_key(':sortdir'):
1202 self.sort = ('-', self.sort[1])
1204 # grouping
1205 self.group = (None, None)
1206 if self.form.has_key(':group'):
1207 group = self.form[':group'].value
1208 if group.startswith('-'):
1209 self.group = ('-', group[1:])
1210 else:
1211 self.group = ('+', group)
1212 if self.form.has_key(':groupdir'):
1213 self.group = ('-', self.group[1])
1215 # filtering
1216 self.filter = []
1217 if self.form.has_key(':filter'):
1218 self.filter = handleListCGIValue(self.form[':filter'])
1219 self.filterspec = {}
1220 if self.classname is not None:
1221 props = self.client.db.getclass(self.classname).getprops()
1222 for name in self.filter:
1223 if self.form.has_key(name):
1224 prop = props[name]
1225 fv = self.form[name]
1226 if (isinstance(prop, hyperdb.Link) or
1227 isinstance(prop, hyperdb.Multilink)):
1228 self.filterspec[name] = handleListCGIValue(fv)
1229 else:
1230 self.filterspec[name] = fv.value
1232 # full-text search argument
1233 self.search_text = None
1234 if self.form.has_key(':search_text'):
1235 self.search_text = self.form[':search_text'].value
1237 # pagination - size and start index
1238 # figure batch args
1239 if self.form.has_key(':pagesize'):
1240 self.pagesize = int(self.form[':pagesize'].value)
1241 else:
1242 self.pagesize = 50
1243 if self.form.has_key(':startwith'):
1244 self.startwith = int(self.form[':startwith'].value)
1245 else:
1246 self.startwith = 0
1248 def updateFromURL(self, url):
1249 ''' Parse the URL for query args, and update my attributes using the
1250 values.
1251 '''
1252 self.form = {}
1253 for name, value in cgi.parse_qsl(url):
1254 if self.form.has_key(name):
1255 if isinstance(self.form[name], type([])):
1256 self.form[name].append(cgi.MiniFieldStorage(name, value))
1257 else:
1258 self.form[name] = [self.form[name],
1259 cgi.MiniFieldStorage(name, value)]
1260 else:
1261 self.form[name] = cgi.MiniFieldStorage(name, value)
1262 self._post_init()
1264 def update(self, kwargs):
1265 ''' Update my attributes using the keyword args
1266 '''
1267 self.__dict__.update(kwargs)
1268 if kwargs.has_key('columns'):
1269 self.show = ShowDict(self.columns)
1271 def description(self):
1272 ''' Return a description of the request - handle for the page title.
1273 '''
1274 s = [self.client.db.config.TRACKER_NAME]
1275 if self.classname:
1276 if self.client.nodeid:
1277 s.append('- %s%s'%(self.classname, self.client.nodeid))
1278 else:
1279 if self.template == 'item':
1280 s.append('- new %s'%self.classname)
1281 elif self.template == 'index':
1282 s.append('- %s index'%self.classname)
1283 else:
1284 s.append('- %s %s'%(self.classname, self.template))
1285 else:
1286 s.append('- home')
1287 return ' '.join(s)
1289 def __str__(self):
1290 d = {}
1291 d.update(self.__dict__)
1292 f = ''
1293 for k in self.form.keys():
1294 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1295 d['form'] = f
1296 e = ''
1297 for k,v in self.env.items():
1298 e += '\n %r=%r'%(k, v)
1299 d['env'] = e
1300 return '''
1301 form: %(form)s
1302 url: %(url)r
1303 base: %(base)r
1304 classname: %(classname)r
1305 template: %(template)r
1306 columns: %(columns)r
1307 sort: %(sort)r
1308 group: %(group)r
1309 filter: %(filter)r
1310 search_text: %(search_text)r
1311 pagesize: %(pagesize)r
1312 startwith: %(startwith)r
1313 env: %(env)s
1314 '''%d
1316 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1317 filterspec=1):
1318 ''' return the current index args as form elements '''
1319 l = []
1320 s = '<input type="hidden" name="%s" value="%s">'
1321 if columns and self.columns:
1322 l.append(s%(':columns', ','.join(self.columns)))
1323 if sort and self.sort[1] is not None:
1324 if self.sort[0] == '-':
1325 val = '-'+self.sort[1]
1326 else:
1327 val = self.sort[1]
1328 l.append(s%(':sort', val))
1329 if group and self.group[1] is not None:
1330 if self.group[0] == '-':
1331 val = '-'+self.group[1]
1332 else:
1333 val = self.group[1]
1334 l.append(s%(':group', val))
1335 if filter and self.filter:
1336 l.append(s%(':filter', ','.join(self.filter)))
1337 if filterspec:
1338 for k,v in self.filterspec.items():
1339 l.append(s%(k, ','.join(v)))
1340 if self.search_text:
1341 l.append(s%(':search_text', self.search_text))
1342 l.append(s%(':pagesize', self.pagesize))
1343 l.append(s%(':startwith', self.startwith))
1344 return '\n'.join(l)
1346 def indexargs_url(self, url, args):
1347 ''' embed the current index args in a URL '''
1348 l = ['%s=%s'%(k,v) for k,v in args.items()]
1349 if self.columns and not args.has_key(':columns'):
1350 l.append(':columns=%s'%(','.join(self.columns)))
1351 if self.sort[1] is not None and not args.has_key(':sort'):
1352 if self.sort[0] == '-':
1353 val = '-'+self.sort[1]
1354 else:
1355 val = self.sort[1]
1356 l.append(':sort=%s'%val)
1357 if self.group[1] is not None and not args.has_key(':group'):
1358 if self.group[0] == '-':
1359 val = '-'+self.group[1]
1360 else:
1361 val = self.group[1]
1362 l.append(':group=%s'%val)
1363 if self.filter and not args.has_key(':columns'):
1364 l.append(':filter=%s'%(','.join(self.filter)))
1365 for k,v in self.filterspec.items():
1366 if not args.has_key(k):
1367 l.append('%s=%s'%(k, ','.join(v)))
1368 if self.search_text and not args.has_key(':search_text'):
1369 l.append(':search_text=%s'%self.search_text)
1370 if not args.has_key(':pagesize'):
1371 l.append(':pagesize=%s'%self.pagesize)
1372 if not args.has_key(':startwith'):
1373 l.append(':startwith=%s'%self.startwith)
1374 return '%s?%s'%(url, '&'.join(l))
1375 indexargs_href = indexargs_url
1377 def base_javascript(self):
1378 return '''
1379 <script language="javascript">
1380 submitted = false;
1381 function submit_once() {
1382 if (submitted) {
1383 alert("Your request is being processed.\\nPlease be patient.");
1384 return 0;
1385 }
1386 submitted = true;
1387 return 1;
1388 }
1390 function help_window(helpurl, width, height) {
1391 HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1392 }
1393 </script>
1394 '''%self.base
1396 def batch(self):
1397 ''' Return a batch object for results from the "current search"
1398 '''
1399 filterspec = self.filterspec
1400 sort = self.sort
1401 group = self.group
1403 # get the list of ids we're batching over
1404 klass = self.client.db.getclass(self.classname)
1405 if self.search_text:
1406 matches = self.client.db.indexer.search(
1407 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1408 else:
1409 matches = None
1410 l = klass.filter(matches, filterspec, sort, group)
1412 # map the item ids to instances
1413 if self.classname == 'user':
1414 klass = HTMLUser
1415 else:
1416 klass = HTMLItem
1417 l = [klass(self.client, self.classname, item) for item in l]
1419 # return the batch object
1420 return Batch(self.client, l, self.pagesize, self.startwith)
1422 # extend the standard ZTUtils Batch object to remove dependency on
1423 # Acquisition and add a couple of useful methods
1424 class Batch(ZTUtils.Batch):
1425 ''' Use me to turn a list of items, or item ids of a given class, into a
1426 series of batches.
1428 ========= ========================================================
1429 Parameter Usage
1430 ========= ========================================================
1431 sequence a list of HTMLItems
1432 size how big to make the sequence.
1433 start where to start (0-indexed) in the sequence.
1434 end where to end (0-indexed) in the sequence.
1435 orphan if the next batch would contain less items than this
1436 value, then it is combined with this batch
1437 overlap the number of items shared between adjacent batches
1438 ========= ========================================================
1440 Attributes: Note that the "start" attribute, unlike the
1441 argument, is a 1-based index (I know, lame). "first" is the
1442 0-based index. "length" is the actual number of elements in
1443 the batch.
1445 "sequence_length" is the length of the original, unbatched, sequence.
1446 '''
1447 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1448 overlap=0):
1449 self.client = client
1450 self.last_index = self.last_item = None
1451 self.current_item = None
1452 self.sequence_length = len(sequence)
1453 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1454 overlap)
1456 # overwrite so we can late-instantiate the HTMLItem instance
1457 def __getitem__(self, index):
1458 if index < 0:
1459 if index + self.end < self.first: raise IndexError, index
1460 return self._sequence[index + self.end]
1462 if index >= self.length:
1463 raise IndexError, index
1465 # move the last_item along - but only if the fetched index changes
1466 # (for some reason, index 0 is fetched twice)
1467 if index != self.last_index:
1468 self.last_item = self.current_item
1469 self.last_index = index
1471 self.current_item = self._sequence[index + self.first]
1472 return self.current_item
1474 def propchanged(self, property):
1475 ''' Detect if the property marked as being the group property
1476 changed in the last iteration fetch
1477 '''
1478 if (self.last_item is None or
1479 self.last_item[property] != self.current_item[property]):
1480 return 1
1481 return 0
1483 # override these 'cos we don't have access to acquisition
1484 def previous(self):
1485 if self.start == 1:
1486 return None
1487 return Batch(self.client, self._sequence, self._size,
1488 self.first - self._size + self.overlap, 0, self.orphan,
1489 self.overlap)
1491 def next(self):
1492 try:
1493 self._sequence[self.end]
1494 except IndexError:
1495 return None
1496 return Batch(self.client, self._sequence, self._size,
1497 self.end - self.overlap, 0, self.orphan, self.overlap)
1499 class TemplatingUtils:
1500 ''' Utilities for templating
1501 '''
1502 def __init__(self, client):
1503 self.client = client
1504 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1505 return Batch(self.client, sequence, size, start, end, orphan,
1506 overlap)