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