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 class NumberHTMLProperty(HTMLProperty):
775 def plain(self):
776 ''' Render a "plain" representation of the property
777 '''
778 return str(self._value)
780 def field(self, size = 30):
781 ''' Render a form edit field for the property
782 '''
783 if self._value is None:
784 value = ''
785 else:
786 value = cgi.escape(str(self._value))
787 value = '"'.join(value.split('"'))
788 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
790 class BooleanHTMLProperty(HTMLProperty):
791 def plain(self):
792 ''' Render a "plain" representation of the property
793 '''
794 if self.value is None:
795 return ''
796 return self._value and "Yes" or "No"
798 def field(self):
799 ''' Render a form edit field for the property
800 '''
801 checked = self._value and "checked" or ""
802 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
803 checked)
804 if checked:
805 checked = ""
806 else:
807 checked = "checked"
808 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
809 checked)
810 return s
812 class DateHTMLProperty(HTMLProperty):
813 def plain(self):
814 ''' Render a "plain" representation of the property
815 '''
816 if self._value is None:
817 return ''
818 return str(self._value)
820 def field(self, size = 30):
821 ''' Render a form edit field for the property
822 '''
823 if self._value is None:
824 value = ''
825 else:
826 value = cgi.escape(str(self._value))
827 value = '"'.join(value.split('"'))
828 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
830 def reldate(self, pretty=1):
831 ''' Render the interval between the date and now.
833 If the "pretty" flag is true, then make the display pretty.
834 '''
835 if not self._value:
836 return ''
838 # figure the interval
839 interval = date.Date('.') - self._value
840 if pretty:
841 return interval.pretty()
842 return str(interval)
844 class IntervalHTMLProperty(HTMLProperty):
845 def plain(self):
846 ''' Render a "plain" representation of the property
847 '''
848 if self._value is None:
849 return ''
850 return str(self._value)
852 def pretty(self):
853 ''' Render the interval in a pretty format (eg. "yesterday")
854 '''
855 return self._value.pretty()
857 def field(self, size = 30):
858 ''' Render a form edit field for the property
859 '''
860 if self._value is None:
861 value = ''
862 else:
863 value = cgi.escape(str(self._value))
864 value = '"'.join(value.split('"'))
865 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
867 class LinkHTMLProperty(HTMLProperty):
868 ''' Link HTMLProperty
869 Include the above as well as being able to access the class
870 information. Stringifying the object itself results in the value
871 from the item being displayed. Accessing attributes of this object
872 result in the appropriate entry from the class being queried for the
873 property accessed (so item/assignedto/name would look up the user
874 entry identified by the assignedto property on item, and then the
875 name property of that user)
876 '''
877 def __getattr__(self, attr):
878 ''' return a new HTMLItem '''
879 #print 'Link.getattr', (self, attr, self._value)
880 if not self._value:
881 raise AttributeError, "Can't access missing value"
882 if self._prop.classname == 'user':
883 klass = HTMLUser
884 else:
885 klass = HTMLItem
886 i = klass(self._client, self._prop.classname, self._value)
887 return getattr(i, attr)
889 def plain(self, escape=0):
890 ''' Render a "plain" representation of the property
891 '''
892 if self._value is None:
893 return ''
894 linkcl = self._db.classes[self._prop.classname]
895 k = linkcl.labelprop(1)
896 value = str(linkcl.get(self._value, k))
897 if escape:
898 value = cgi.escape(value)
899 return value
901 def field(self, showid=0, size=None):
902 ''' Render a form edit field for the property
903 '''
904 linkcl = self._db.getclass(self._prop.classname)
905 if linkcl.getprops().has_key('order'):
906 sort_on = 'order'
907 else:
908 sort_on = linkcl.labelprop()
909 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
910 # TODO: make this a field display, not a menu one!
911 l = ['<select name="%s">'%self._name]
912 k = linkcl.labelprop(1)
913 if self._value is None:
914 s = 'selected '
915 else:
916 s = ''
917 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
918 for optionid in options:
919 option = linkcl.get(optionid, k)
920 s = ''
921 if optionid == self._value:
922 s = 'selected '
923 if showid:
924 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
925 else:
926 lab = option
927 if size is not None and len(lab) > size:
928 lab = lab[:size-3] + '...'
929 lab = cgi.escape(lab)
930 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
931 l.append('</select>')
932 return '\n'.join(l)
934 def menu(self, size=None, height=None, showid=0, additional=[],
935 **conditions):
936 ''' Render a form select list for this property
937 '''
938 value = self._value
940 # sort function
941 sortfunc = make_sort_function(self._db, self._prop.classname)
943 # force the value to be a single choice
944 if isinstance(value, type('')):
945 value = value[0]
946 linkcl = self._db.getclass(self._prop.classname)
947 l = ['<select name="%s">'%self._name]
948 k = linkcl.labelprop(1)
949 s = ''
950 if value is None:
951 s = 'selected '
952 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
953 if linkcl.getprops().has_key('order'):
954 sort_on = ('+', 'order')
955 else:
956 sort_on = ('+', linkcl.labelprop())
957 options = linkcl.filter(None, conditions, sort_on, (None, None))
958 for optionid in options:
959 option = linkcl.get(optionid, k)
960 s = ''
961 if value in [optionid, option]:
962 s = 'selected '
963 if showid:
964 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
965 else:
966 lab = option
967 if size is not None and len(lab) > size:
968 lab = lab[:size-3] + '...'
969 if additional:
970 m = []
971 for propname in additional:
972 m.append(linkcl.get(optionid, propname))
973 lab = lab + ' (%s)'%', '.join(map(str, m))
974 lab = cgi.escape(lab)
975 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
976 l.append('</select>')
977 return '\n'.join(l)
978 # def checklist(self, ...)
980 class MultilinkHTMLProperty(HTMLProperty):
981 ''' Multilink HTMLProperty
983 Also be iterable, returning a wrapper object like the Link case for
984 each entry in the multilink.
985 '''
986 def __len__(self):
987 ''' length of the multilink '''
988 return len(self._value)
990 def __getattr__(self, attr):
991 ''' no extended attribute accesses make sense here '''
992 raise AttributeError, attr
994 def __getitem__(self, num):
995 ''' iterate and return a new HTMLItem
996 '''
997 #print 'Multi.getitem', (self, num)
998 value = self._value[num]
999 if self._prop.classname == 'user':
1000 klass = HTMLUser
1001 else:
1002 klass = HTMLItem
1003 return klass(self._client, self._prop.classname, value)
1005 def __contains__(self, value):
1006 ''' Support the "in" operator
1007 '''
1008 return value in self._value
1010 def reverse(self):
1011 ''' return the list in reverse order
1012 '''
1013 l = self._value[:]
1014 l.reverse()
1015 if self._prop.classname == 'user':
1016 klass = HTMLUser
1017 else:
1018 klass = HTMLItem
1019 return [klass(self._client, self._prop.classname, value) for value in l]
1021 def plain(self, escape=0):
1022 ''' Render a "plain" representation of the property
1023 '''
1024 linkcl = self._db.classes[self._prop.classname]
1025 k = linkcl.labelprop(1)
1026 labels = []
1027 for v in self._value:
1028 labels.append(linkcl.get(v, k))
1029 value = ', '.join(labels)
1030 if escape:
1031 value = cgi.escape(value)
1032 return value
1034 def field(self, size=30, showid=0):
1035 ''' Render a form edit field for the property
1036 '''
1037 sortfunc = make_sort_function(self._db, self._prop.classname)
1038 linkcl = self._db.getclass(self._prop.classname)
1039 value = self._value[:]
1040 if value:
1041 value.sort(sortfunc)
1042 # map the id to the label property
1043 if not linkcl.getkey():
1044 showid=1
1045 if not showid:
1046 k = linkcl.labelprop(1)
1047 value = [linkcl.get(v, k) for v in value]
1048 value = cgi.escape(','.join(value))
1049 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1051 def menu(self, size=None, height=None, showid=0, additional=[],
1052 **conditions):
1053 ''' Render a form select list for this property
1054 '''
1055 value = self._value
1057 # sort function
1058 sortfunc = make_sort_function(self._db, self._prop.classname)
1060 linkcl = self._db.getclass(self._prop.classname)
1061 if linkcl.getprops().has_key('order'):
1062 sort_on = ('+', 'order')
1063 else:
1064 sort_on = ('+', linkcl.labelprop())
1065 options = linkcl.filter(None, conditions, sort_on, (None,None))
1066 height = height or min(len(options), 7)
1067 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1068 k = linkcl.labelprop(1)
1069 for optionid in options:
1070 option = linkcl.get(optionid, k)
1071 s = ''
1072 if optionid in value or option in value:
1073 s = 'selected '
1074 if showid:
1075 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1076 else:
1077 lab = option
1078 if size is not None and len(lab) > size:
1079 lab = lab[:size-3] + '...'
1080 if additional:
1081 m = []
1082 for propname in additional:
1083 m.append(linkcl.get(optionid, propname))
1084 lab = lab + ' (%s)'%', '.join(m)
1085 lab = cgi.escape(lab)
1086 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1087 lab))
1088 l.append('</select>')
1089 return '\n'.join(l)
1091 # set the propclasses for HTMLItem
1092 propclasses = (
1093 (hyperdb.String, StringHTMLProperty),
1094 (hyperdb.Number, NumberHTMLProperty),
1095 (hyperdb.Boolean, BooleanHTMLProperty),
1096 (hyperdb.Date, DateHTMLProperty),
1097 (hyperdb.Interval, IntervalHTMLProperty),
1098 (hyperdb.Password, PasswordHTMLProperty),
1099 (hyperdb.Link, LinkHTMLProperty),
1100 (hyperdb.Multilink, MultilinkHTMLProperty),
1101 )
1103 def make_sort_function(db, classname):
1104 '''Make a sort function for a given class
1105 '''
1106 linkcl = db.getclass(classname)
1107 if linkcl.getprops().has_key('order'):
1108 sort_on = 'order'
1109 else:
1110 sort_on = linkcl.labelprop()
1111 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1112 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1113 return sortfunc
1115 def handleListCGIValue(value):
1116 ''' Value is either a single item or a list of items. Each item has a
1117 .value that we're actually interested in.
1118 '''
1119 if isinstance(value, type([])):
1120 return [value.value for value in value]
1121 else:
1122 value = value.value.strip()
1123 if not value:
1124 return []
1125 return value.split(',')
1127 class ShowDict:
1128 ''' A convenience access to the :columns index parameters
1129 '''
1130 def __init__(self, columns):
1131 self.columns = {}
1132 for col in columns:
1133 self.columns[col] = 1
1134 def __getitem__(self, name):
1135 return self.columns.has_key(name)
1137 class HTMLRequest:
1138 ''' The *request*, holding the CGI form and environment.
1140 "form" the CGI form as a cgi.FieldStorage
1141 "env" the CGI environment variables
1142 "url" the current URL path for this request
1143 "base" the base URL for this instance
1144 "user" a HTMLUser instance for this user
1145 "classname" the current classname (possibly None)
1146 "template" the current template (suffix, also possibly None)
1148 Index args:
1149 "columns" dictionary of the columns to display in an index page
1150 "show" a convenience access to columns - request/show/colname will
1151 be true if the columns should be displayed, false otherwise
1152 "sort" index sort column (direction, column name)
1153 "group" index grouping property (direction, column name)
1154 "filter" properties to filter the index on
1155 "filterspec" values to filter the index on
1156 "search_text" text to perform a full-text search on for an index
1158 '''
1159 def __init__(self, client):
1160 self.client = client
1162 # easier access vars
1163 self.form = client.form
1164 self.env = client.env
1165 self.base = client.base
1166 self.url = client.url
1167 self.user = HTMLUser(client, 'user', client.userid)
1169 # store the current class name and action
1170 self.classname = client.classname
1171 self.template = client.template
1173 self._post_init()
1175 def _post_init(self):
1176 ''' Set attributes based on self.form
1177 '''
1178 # extract the index display information from the form
1179 self.columns = []
1180 if self.form.has_key(':columns'):
1181 self.columns = handleListCGIValue(self.form[':columns'])
1182 self.show = ShowDict(self.columns)
1184 # sorting
1185 self.sort = (None, None)
1186 if self.form.has_key(':sort'):
1187 sort = self.form[':sort'].value
1188 if sort.startswith('-'):
1189 self.sort = ('-', sort[1:])
1190 else:
1191 self.sort = ('+', sort)
1192 if self.form.has_key(':sortdir'):
1193 self.sort = ('-', self.sort[1])
1195 # grouping
1196 self.group = (None, None)
1197 if self.form.has_key(':group'):
1198 group = self.form[':group'].value
1199 if group.startswith('-'):
1200 self.group = ('-', group[1:])
1201 else:
1202 self.group = ('+', group)
1203 if self.form.has_key(':groupdir'):
1204 self.group = ('-', self.group[1])
1206 # filtering
1207 self.filter = []
1208 if self.form.has_key(':filter'):
1209 self.filter = handleListCGIValue(self.form[':filter'])
1210 self.filterspec = {}
1211 if self.classname is not None:
1212 props = self.client.db.getclass(self.classname).getprops()
1213 for name in self.filter:
1214 if self.form.has_key(name):
1215 prop = props[name]
1216 fv = self.form[name]
1217 if (isinstance(prop, hyperdb.Link) or
1218 isinstance(prop, hyperdb.Multilink)):
1219 self.filterspec[name] = handleListCGIValue(fv)
1220 else:
1221 self.filterspec[name] = fv.value
1223 # full-text search argument
1224 self.search_text = None
1225 if self.form.has_key(':search_text'):
1226 self.search_text = self.form[':search_text'].value
1228 # pagination - size and start index
1229 # figure batch args
1230 if self.form.has_key(':pagesize'):
1231 self.pagesize = int(self.form[':pagesize'].value)
1232 else:
1233 self.pagesize = 50
1234 if self.form.has_key(':startwith'):
1235 self.startwith = int(self.form[':startwith'].value)
1236 else:
1237 self.startwith = 0
1239 def updateFromURL(self, url):
1240 ''' Parse the URL for query args, and update my attributes using the
1241 values.
1242 '''
1243 self.form = {}
1244 for name, value in cgi.parse_qsl(url):
1245 if self.form.has_key(name):
1246 if isinstance(self.form[name], type([])):
1247 self.form[name].append(cgi.MiniFieldStorage(name, value))
1248 else:
1249 self.form[name] = [self.form[name],
1250 cgi.MiniFieldStorage(name, value)]
1251 else:
1252 self.form[name] = cgi.MiniFieldStorage(name, value)
1253 self._post_init()
1255 def update(self, kwargs):
1256 ''' Update my attributes using the keyword args
1257 '''
1258 self.__dict__.update(kwargs)
1259 if kwargs.has_key('columns'):
1260 self.show = ShowDict(self.columns)
1262 def description(self):
1263 ''' Return a description of the request - handle for the page title.
1264 '''
1265 s = [self.client.db.config.TRACKER_NAME]
1266 if self.classname:
1267 if self.client.nodeid:
1268 s.append('- %s%s'%(self.classname, self.client.nodeid))
1269 else:
1270 if self.template == 'item':
1271 s.append('- new %s'%self.classname)
1272 elif self.template == 'index':
1273 s.append('- %s index'%self.classname)
1274 else:
1275 s.append('- %s %s'%(self.classname, self.template))
1276 else:
1277 s.append('- home')
1278 return ' '.join(s)
1280 def __str__(self):
1281 d = {}
1282 d.update(self.__dict__)
1283 f = ''
1284 for k in self.form.keys():
1285 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1286 d['form'] = f
1287 e = ''
1288 for k,v in self.env.items():
1289 e += '\n %r=%r'%(k, v)
1290 d['env'] = e
1291 return '''
1292 form: %(form)s
1293 url: %(url)r
1294 base: %(base)r
1295 classname: %(classname)r
1296 template: %(template)r
1297 columns: %(columns)r
1298 sort: %(sort)r
1299 group: %(group)r
1300 filter: %(filter)r
1301 search_text: %(search_text)r
1302 pagesize: %(pagesize)r
1303 startwith: %(startwith)r
1304 env: %(env)s
1305 '''%d
1307 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1308 filterspec=1):
1309 ''' return the current index args as form elements '''
1310 l = []
1311 s = '<input type="hidden" name="%s" value="%s">'
1312 if columns and self.columns:
1313 l.append(s%(':columns', ','.join(self.columns)))
1314 if sort and self.sort[1] is not None:
1315 if self.sort[0] == '-':
1316 val = '-'+self.sort[1]
1317 else:
1318 val = self.sort[1]
1319 l.append(s%(':sort', val))
1320 if group and self.group[1] is not None:
1321 if self.group[0] == '-':
1322 val = '-'+self.group[1]
1323 else:
1324 val = self.group[1]
1325 l.append(s%(':group', val))
1326 if filter and self.filter:
1327 l.append(s%(':filter', ','.join(self.filter)))
1328 if filterspec:
1329 for k,v in self.filterspec.items():
1330 l.append(s%(k, ','.join(v)))
1331 if self.search_text:
1332 l.append(s%(':search_text', self.search_text))
1333 l.append(s%(':pagesize', self.pagesize))
1334 l.append(s%(':startwith', self.startwith))
1335 return '\n'.join(l)
1337 def indexargs_url(self, url, args):
1338 ''' embed the current index args in a URL '''
1339 l = ['%s=%s'%(k,v) for k,v in args.items()]
1340 if self.columns and not args.has_key(':columns'):
1341 l.append(':columns=%s'%(','.join(self.columns)))
1342 if self.sort[1] is not None and not args.has_key(':sort'):
1343 if self.sort[0] == '-':
1344 val = '-'+self.sort[1]
1345 else:
1346 val = self.sort[1]
1347 l.append(':sort=%s'%val)
1348 if self.group[1] is not None and not args.has_key(':group'):
1349 if self.group[0] == '-':
1350 val = '-'+self.group[1]
1351 else:
1352 val = self.group[1]
1353 l.append(':group=%s'%val)
1354 if self.filter and not args.has_key(':columns'):
1355 l.append(':filter=%s'%(','.join(self.filter)))
1356 for k,v in self.filterspec.items():
1357 if not args.has_key(k):
1358 l.append('%s=%s'%(k, ','.join(v)))
1359 if self.search_text and not args.has_key(':search_text'):
1360 l.append(':search_text=%s'%self.search_text)
1361 if not args.has_key(':pagesize'):
1362 l.append(':pagesize=%s'%self.pagesize)
1363 if not args.has_key(':startwith'):
1364 l.append(':startwith=%s'%self.startwith)
1365 return '%s?%s'%(url, '&'.join(l))
1366 indexargs_href = indexargs_url
1368 def base_javascript(self):
1369 return '''
1370 <script language="javascript">
1371 submitted = false;
1372 function submit_once() {
1373 if (submitted) {
1374 alert("Your request is being processed.\\nPlease be patient.");
1375 return 0;
1376 }
1377 submitted = true;
1378 return 1;
1379 }
1381 function help_window(helpurl, width, height) {
1382 HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1383 }
1384 </script>
1385 '''%self.base
1387 def batch(self):
1388 ''' Return a batch object for results from the "current search"
1389 '''
1390 filterspec = self.filterspec
1391 sort = self.sort
1392 group = self.group
1394 # get the list of ids we're batching over
1395 klass = self.client.db.getclass(self.classname)
1396 if self.search_text:
1397 matches = self.client.db.indexer.search(
1398 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1399 else:
1400 matches = None
1401 l = klass.filter(matches, filterspec, sort, group)
1403 # map the item ids to instances
1404 if self.classname == 'user':
1405 klass = HTMLUser
1406 else:
1407 klass = HTMLItem
1408 l = [klass(self.client, self.classname, item) for item in l]
1410 # return the batch object
1411 return Batch(self.client, l, self.pagesize, self.startwith)
1413 # extend the standard ZTUtils Batch object to remove dependency on
1414 # Acquisition and add a couple of useful methods
1415 class Batch(ZTUtils.Batch):
1416 ''' Use me to turn a list of items, or item ids of a given class, into a
1417 series of batches.
1419 ========= ========================================================
1420 Parameter Usage
1421 ========= ========================================================
1422 sequence a list of HTMLItems
1423 size how big to make the sequence.
1424 start where to start (0-indexed) in the sequence.
1425 end where to end (0-indexed) in the sequence.
1426 orphan if the next batch would contain less items than this
1427 value, then it is combined with this batch
1428 overlap the number of items shared between adjacent batches
1429 ========= ========================================================
1431 Attributes: Note that the "start" attribute, unlike the
1432 argument, is a 1-based index (I know, lame). "first" is the
1433 0-based index. "length" is the actual number of elements in
1434 the batch.
1436 "sequence_length" is the length of the original, unbatched, sequence.
1437 '''
1438 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1439 overlap=0):
1440 self.client = client
1441 self.last_index = self.last_item = None
1442 self.current_item = None
1443 self.sequence_length = len(sequence)
1444 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1445 overlap)
1447 # overwrite so we can late-instantiate the HTMLItem instance
1448 def __getitem__(self, index):
1449 if index < 0:
1450 if index + self.end < self.first: raise IndexError, index
1451 return self._sequence[index + self.end]
1453 if index >= self.length:
1454 raise IndexError, index
1456 # move the last_item along - but only if the fetched index changes
1457 # (for some reason, index 0 is fetched twice)
1458 if index != self.last_index:
1459 self.last_item = self.current_item
1460 self.last_index = index
1462 self.current_item = self._sequence[index + self.first]
1463 return self.current_item
1465 def propchanged(self, property):
1466 ''' Detect if the property marked as being the group property
1467 changed in the last iteration fetch
1468 '''
1469 if (self.last_item is None or
1470 self.last_item[property] != self.current_item[property]):
1471 return 1
1472 return 0
1474 # override these 'cos we don't have access to acquisition
1475 def previous(self):
1476 if self.start == 1:
1477 return None
1478 return Batch(self.client, self._sequence, self._size,
1479 self.first - self._size + self.overlap, 0, self.orphan,
1480 self.overlap)
1482 def next(self):
1483 try:
1484 self._sequence[self.end]
1485 except IndexError:
1486 return None
1487 return Batch(self.client, self._sequence, self._size,
1488 self.end - self.overlap, 0, self.orphan, self.overlap)
1490 class TemplatingUtils:
1491 ''' Utilities for templating
1492 '''
1493 def __init__(self, client):
1494 self.client = client
1495 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1496 return Batch(self.client, sequence, size, start, end, orphan,
1497 overlap)