4a154b91394f1b071ee4c634b3fbd0d9e2432b89
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 c['context'] = HTMLItem(client, classname, client.nodeid)
159 else:
160 c['context'] = HTMLClass(client, classname)
161 return c
163 def render(self, client, classname, request, **options):
164 """Render this Page Template"""
166 if not self._v_cooked:
167 self._cook()
169 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
171 if self._v_errors:
172 raise PageTemplate.PTRuntimeError, \
173 'Page Template %s has errors.' % self.id
175 # figure the context
176 classname = classname or client.classname
177 request = request or HTMLRequest(client)
178 c = self.getContext(client, classname, request)
179 c.update({'options': options})
181 # and go
182 output = StringIO.StringIO()
183 TALInterpreter(self._v_program, self._v_macros,
184 getEngine().getContext(c), output, tal=1, strictinsert=0)()
185 return output.getvalue()
187 class HTMLDatabase:
188 ''' Return HTMLClasses for valid class fetches
189 '''
190 def __init__(self, client):
191 self._client = client
193 # we want config to be exposed
194 self.config = client.db.config
196 def __getattr__(self, attr):
197 try:
198 self._client.db.getclass(attr)
199 except KeyError:
200 raise AttributeError, attr
201 return HTMLClass(self._client, attr)
202 def classes(self):
203 l = self._client.db.classes.keys()
204 l.sort()
205 return [HTMLClass(self._client, cn) for cn in l]
207 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
208 cl = db.getclass(prop.classname)
209 l = []
210 for entry in ids:
211 if num_re.match(entry):
212 l.append(entry)
213 else:
214 l.append(cl.lookup(entry))
215 return l
217 class HTMLClass:
218 ''' Accesses through a class (either through *class* or *db.<classname>*)
219 '''
220 def __init__(self, client, classname):
221 self._client = client
222 self._db = client.db
224 # we want classname to be exposed
225 self.classname = classname
226 if classname is not None:
227 self._klass = self._db.getclass(self.classname)
228 self._props = self._klass.getprops()
230 def __repr__(self):
231 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
233 def __getitem__(self, item):
234 ''' return an HTMLProperty instance
235 '''
236 #print 'HTMLClass.getitem', (self, item)
238 # we don't exist
239 if item == 'id':
240 return None
242 # get the property
243 prop = self._props[item]
245 # look up the correct HTMLProperty class
246 form = self._client.form
247 for klass, htmlklass in propclasses:
248 if not isinstance(prop, klass):
249 continue
250 if form.has_key(item):
251 if isinstance(prop, hyperdb.Multilink):
252 value = lookupIds(self._db, prop,
253 handleListCGIValue(form[item]))
254 elif isinstance(prop, hyperdb.Link):
255 value = form[item].value.strip()
256 if value:
257 value = lookupIds(self._db, prop, [value])[0]
258 else:
259 value = None
260 else:
261 value = form[item].value.strip() or None
262 else:
263 if isinstance(prop, hyperdb.Multilink):
264 value = []
265 else:
266 value = None
267 return htmlklass(self._client, '', prop, item, value)
269 # no good
270 raise KeyError, item
272 def __getattr__(self, attr):
273 ''' convenience access '''
274 try:
275 return self[attr]
276 except KeyError:
277 raise AttributeError, attr
279 def properties(self):
280 ''' Return HTMLProperty for all of this class' properties.
281 '''
282 l = []
283 for name, prop in self._props.items():
284 for klass, htmlklass in propclasses:
285 if isinstance(prop, hyperdb.Multilink):
286 value = []
287 else:
288 value = None
289 if isinstance(prop, klass):
290 l.append(htmlklass(self._client, '', prop, name, value))
291 return l
293 def list(self):
294 ''' List all items in this class.
295 '''
296 if self.classname == 'user':
297 klass = HTMLUser
298 else:
299 klass = HTMLItem
300 l = [klass(self._client, self.classname, x) for x in self._klass.list()]
301 return l
303 def csv(self):
304 ''' Return the items of this class as a chunk of CSV text.
305 '''
306 # get the CSV module
307 try:
308 import csv
309 except ImportError:
310 return 'Sorry, you need the csv module to use this function.\n'\
311 'Get it from: http://www.object-craft.com.au/projects/csv/'
313 props = self.propnames()
314 p = csv.parser()
315 s = StringIO.StringIO()
316 s.write(p.join(props) + '\n')
317 for nodeid in self._klass.list():
318 l = []
319 for name in props:
320 value = self._klass.get(nodeid, name)
321 if value is None:
322 l.append('')
323 elif isinstance(value, type([])):
324 l.append(':'.join(map(str, value)))
325 else:
326 l.append(str(self._klass.get(nodeid, name)))
327 s.write(p.join(l) + '\n')
328 return s.getvalue()
330 def propnames(self):
331 ''' Return the list of the names of the properties of this class.
332 '''
333 idlessprops = self._klass.getprops(protected=0).keys()
334 idlessprops.sort()
335 return ['id'] + idlessprops
337 def filter(self, request=None):
338 ''' Return a list of items from this class, filtered and sorted
339 by the current requested filterspec/filter/sort/group args
340 '''
341 if request is not None:
342 filterspec = request.filterspec
343 sort = request.sort
344 group = request.group
345 if self.classname == 'user':
346 klass = HTMLUser
347 else:
348 klass = HTMLItem
349 l = [klass(self._client, self.classname, x)
350 for x in self._klass.filter(None, filterspec, sort, group)]
351 return l
353 def classhelp(self, properties=None, label='list', width='500',
354 height='400'):
355 ''' Pop up a javascript window with class help
357 This generates a link to a popup window which displays the
358 properties indicated by "properties" of the class named by
359 "classname". The "properties" should be a comma-separated list
360 (eg. 'id,name,description'). Properties defaults to all the
361 properties of a class (excluding id, creator, created and
362 activity).
364 You may optionally override the label displayed, the width and
365 height. The popup window will be resizable and scrollable.
366 '''
367 if properties is None:
368 properties = self._klass.getprops(protected=0).keys()
369 properties.sort()
370 properties = ','.join(properties)
371 return '<a href="javascript:help_window(\'%s?:template=help&' \
372 ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
373 '(%s)</b></a>'%(self.classname, properties, width, height, label)
375 def submit(self, label="Submit New Entry"):
376 ''' Generate a submit button (and action hidden element)
377 '''
378 return ' <input type="hidden" name=":action" value="new">\n'\
379 ' <input type="submit" name="submit" value="%s">'%label
381 def history(self):
382 return 'New node - no history'
384 def renderWith(self, name, **kwargs):
385 ''' Render this class with the given template.
386 '''
387 # create a new request and override the specified args
388 req = HTMLRequest(self._client)
389 req.classname = self.classname
390 req.update(kwargs)
392 # new template, using the specified classname and request
393 pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
395 # use our fabricated request
396 return pt.render(self._client, self.classname, req)
398 class HTMLItem:
399 ''' Accesses through an *item*
400 '''
401 def __init__(self, client, classname, nodeid):
402 self._client = client
403 self._db = client.db
404 self._classname = classname
405 self._nodeid = nodeid
406 self._klass = self._db.getclass(classname)
407 self._props = self._klass.getprops()
409 def __repr__(self):
410 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
411 self._nodeid)
413 def __getitem__(self, item):
414 ''' return an HTMLProperty instance
415 '''
416 #print 'HTMLItem.getitem', (self, item)
417 if item == 'id':
418 return self._nodeid
420 # get the property
421 prop = self._props[item]
423 # get the value, handling missing values
424 value = self._klass.get(self._nodeid, item, None)
425 if value is None:
426 if isinstance(self._props[item], hyperdb.Multilink):
427 value = []
429 # look up the correct HTMLProperty class
430 for klass, htmlklass in propclasses:
431 if isinstance(prop, klass):
432 return htmlklass(self._client, self._nodeid, prop, item, value)
434 raise KeyErorr, item
436 def __getattr__(self, attr):
437 ''' convenience access to properties '''
438 try:
439 return self[attr]
440 except KeyError:
441 raise AttributeError, attr
443 def submit(self, label="Submit Changes"):
444 ''' Generate a submit button (and action hidden element)
445 '''
446 return ' <input type="hidden" name=":action" value="edit">\n'\
447 ' <input type="submit" name="submit" value="%s">'%label
449 def journal(self, direction='descending'):
450 ''' Return a list of HTMLJournalEntry instances.
451 '''
452 # XXX do this
453 return []
455 def history(self, direction='descending'):
456 l = ['<table class="history">'
457 '<tr><th colspan="4" class="header">',
458 _('History'),
459 '</th></tr><tr>',
460 _('<th>Date</th>'),
461 _('<th>User</th>'),
462 _('<th>Action</th>'),
463 _('<th>Args</th>'),
464 '</tr>']
465 comments = {}
466 history = self._klass.history(self._nodeid)
467 history.sort()
468 if direction == 'descending':
469 history.reverse()
470 for id, evt_date, user, action, args in history:
471 date_s = str(evt_date).replace("."," ")
472 arg_s = ''
473 if action == 'link' and type(args) == type(()):
474 if len(args) == 3:
475 linkcl, linkid, key = args
476 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
477 linkcl, linkid, key)
478 else:
479 arg_s = str(args)
481 elif action == 'unlink' and type(args) == type(()):
482 if len(args) == 3:
483 linkcl, linkid, key = args
484 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
485 linkcl, linkid, key)
486 else:
487 arg_s = str(args)
489 elif type(args) == type({}):
490 cell = []
491 for k in args.keys():
492 # try to get the relevant property and treat it
493 # specially
494 try:
495 prop = self._props[k]
496 except KeyError:
497 prop = None
498 if prop is not None:
499 if args[k] and (isinstance(prop, hyperdb.Multilink) or
500 isinstance(prop, hyperdb.Link)):
501 # figure what the link class is
502 classname = prop.classname
503 try:
504 linkcl = self._db.getclass(classname)
505 except KeyError:
506 labelprop = None
507 comments[classname] = _('''The linked class
508 %(classname)s no longer exists''')%locals()
509 labelprop = linkcl.labelprop(1)
510 hrefable = os.path.exists(
511 os.path.join(self._db.config.TEMPLATES,
512 classname+'.item'))
514 if isinstance(prop, hyperdb.Multilink) and \
515 len(args[k]) > 0:
516 ml = []
517 for linkid in args[k]:
518 if isinstance(linkid, type(())):
519 sublabel = linkid[0] + ' '
520 linkids = linkid[1]
521 else:
522 sublabel = ''
523 linkids = [linkid]
524 subml = []
525 for linkid in linkids:
526 label = classname + linkid
527 # if we have a label property, try to use it
528 # TODO: test for node existence even when
529 # there's no labelprop!
530 try:
531 if labelprop is not None:
532 label = linkcl.get(linkid, labelprop)
533 except IndexError:
534 comments['no_link'] = _('''<strike>The
535 linked node no longer
536 exists</strike>''')
537 subml.append('<strike>%s</strike>'%label)
538 else:
539 if hrefable:
540 subml.append('<a href="%s%s">%s</a>'%(
541 classname, linkid, label))
542 ml.append(sublabel + ', '.join(subml))
543 cell.append('%s:\n %s'%(k, ', '.join(ml)))
544 elif isinstance(prop, hyperdb.Link) and args[k]:
545 label = classname + args[k]
546 # if we have a label property, try to use it
547 # TODO: test for node existence even when
548 # there's no labelprop!
549 if labelprop is not None:
550 try:
551 label = linkcl.get(args[k], labelprop)
552 except IndexError:
553 comments['no_link'] = _('''<strike>The
554 linked node no longer
555 exists</strike>''')
556 cell.append(' <strike>%s</strike>,\n'%label)
557 # "flag" this is done .... euwww
558 label = None
559 if label is not None:
560 if hrefable:
561 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
562 classname, args[k], label))
563 else:
564 cell.append('%s: %s' % (k,label))
566 elif isinstance(prop, hyperdb.Date) and args[k]:
567 d = date.Date(args[k])
568 cell.append('%s: %s'%(k, str(d)))
570 elif isinstance(prop, hyperdb.Interval) and args[k]:
571 d = date.Interval(args[k])
572 cell.append('%s: %s'%(k, str(d)))
574 elif isinstance(prop, hyperdb.String) and args[k]:
575 cell.append('%s: %s'%(k, cgi.escape(args[k])))
577 elif not args[k]:
578 cell.append('%s: (no value)\n'%k)
580 else:
581 cell.append('%s: %s\n'%(k, str(args[k])))
582 else:
583 # property no longer exists
584 comments['no_exist'] = _('''<em>The indicated property
585 no longer exists</em>''')
586 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
587 arg_s = '<br />'.join(cell)
588 else:
589 # unkown event!!
590 comments['unknown'] = _('''<strong><em>This event is not
591 handled by the history display!</em></strong>''')
592 arg_s = '<strong><em>' + str(args) + '</em></strong>'
593 date_s = date_s.replace(' ', ' ')
594 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
595 date_s, user, action, arg_s))
596 if comments:
597 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
598 for entry in comments.values():
599 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
600 l.append('</table>')
601 return '\n'.join(l)
603 def renderQueryForm(self):
604 ''' Render this item, which is a query, as a search form.
605 '''
606 # create a new request and override the specified args
607 req = HTMLRequest(self._client)
608 req.classname = self._klass.get(self._nodeid, 'klass')
609 req.updateFromURL(self._klass.get(self._nodeid, 'url'))
611 # new template, using the specified classname and request
612 pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
614 # use our fabricated request
615 return pt.render(self._client, req.classname, req)
617 class HTMLUser(HTMLItem):
618 ''' Accesses through the *user* (a special case of item)
619 '''
620 def __init__(self, client, classname, nodeid):
621 HTMLItem.__init__(self, client, 'user', nodeid)
622 self._default_classname = client.classname
624 # used for security checks
625 self._security = client.db.security
626 _marker = []
627 def hasPermission(self, role, classname=_marker):
628 ''' Determine if the user has the Role.
630 The class being tested defaults to the template's class, but may
631 be overidden for this test by suppling an alternate classname.
632 '''
633 if classname is self._marker:
634 classname = self._default_classname
635 return self._security.hasPermission(role, self._nodeid, classname)
637 class HTMLProperty:
638 ''' String, Number, Date, Interval HTMLProperty
640 Has useful attributes:
642 _name the name of the property
643 _value the value of the property if any
645 A wrapper object which may be stringified for the plain() behaviour.
646 '''
647 def __init__(self, client, nodeid, prop, name, value):
648 self._client = client
649 self._db = client.db
650 self._nodeid = nodeid
651 self._prop = prop
652 self._name = name
653 self._value = value
654 def __repr__(self):
655 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
656 def __str__(self):
657 return self.plain()
658 def __cmp__(self, other):
659 if isinstance(other, HTMLProperty):
660 return cmp(self._value, other._value)
661 return cmp(self._value, other)
663 class StringHTMLProperty(HTMLProperty):
664 def plain(self, escape=0):
665 ''' Render a "plain" representation of the property
666 '''
667 if self._value is None:
668 return ''
669 if escape:
670 return cgi.escape(str(self._value))
671 return str(self._value)
673 def stext(self, escape=0):
674 ''' Render the value of the property as StructuredText.
676 This requires the StructureText module to be installed separately.
677 '''
678 s = self.plain(escape=escape)
679 if not StructuredText:
680 return s
681 return StructuredText(s,level=1,header=0)
683 def field(self, size = 30):
684 ''' Render a form edit field for the property
685 '''
686 if self._value is None:
687 value = ''
688 else:
689 value = cgi.escape(str(self._value))
690 value = '"'.join(value.split('"'))
691 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
693 def multiline(self, escape=0, rows=5, cols=40):
694 ''' Render a multiline form edit field for the property
695 '''
696 if self._value is None:
697 value = ''
698 else:
699 value = cgi.escape(str(self._value))
700 value = '"'.join(value.split('"'))
701 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
702 self._name, rows, cols, value)
704 def email(self, escape=1):
705 ''' Render the value of the property as an obscured email address
706 '''
707 if self._value is None: value = ''
708 else: value = str(self._value)
709 value = value.replace('@', ' at ')
710 value = value.replace('.', ' ')
711 if escape:
712 value = cgi.escape(value)
713 return value
715 class PasswordHTMLProperty(HTMLProperty):
716 def plain(self):
717 ''' Render a "plain" representation of the property
718 '''
719 if self._value is None:
720 return ''
721 return _('*encrypted*')
723 def field(self, size = 30):
724 ''' Render a form edit field for the property
725 '''
726 return '<input type="password" name="%s" size="%s">'%(self._name, size)
728 class NumberHTMLProperty(HTMLProperty):
729 def plain(self):
730 ''' Render a "plain" representation of the property
731 '''
732 return str(self._value)
734 def field(self, size = 30):
735 ''' Render a 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 '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
744 class BooleanHTMLProperty(HTMLProperty):
745 def plain(self):
746 ''' Render a "plain" representation of the property
747 '''
748 if self.value is None:
749 return ''
750 return self._value and "Yes" or "No"
752 def field(self):
753 ''' Render a form edit field for the property
754 '''
755 checked = self._value and "checked" or ""
756 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
757 checked)
758 if checked:
759 checked = ""
760 else:
761 checked = "checked"
762 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
763 checked)
764 return s
766 class DateHTMLProperty(HTMLProperty):
767 def plain(self):
768 ''' Render a "plain" representation of the property
769 '''
770 if self._value is None:
771 return ''
772 return str(self._value)
774 def field(self, size = 30):
775 ''' Render a form edit field for the property
776 '''
777 if self._value is None:
778 value = ''
779 else:
780 value = cgi.escape(str(self._value))
781 value = '"'.join(value.split('"'))
782 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
784 def reldate(self, pretty=1):
785 if not self._value:
786 return ''
788 # figure the interval
789 interval = date.Date('.') - self._value
790 if pretty:
791 return interval.pretty()
792 return str(interval)
794 class IntervalHTMLProperty(HTMLProperty):
795 def plain(self):
796 ''' Render a "plain" representation of the property
797 '''
798 if self._value is None:
799 return ''
800 return str(self._value)
802 def pretty(self):
803 return self._value.pretty()
805 def field(self, size = 30):
806 ''' Render a form edit field for the property
807 '''
808 if self._value is None:
809 value = ''
810 else:
811 value = cgi.escape(str(self._value))
812 value = '"'.join(value.split('"'))
813 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
815 class LinkHTMLProperty(HTMLProperty):
816 ''' Link HTMLProperty
817 Include the above as well as being able to access the class
818 information. Stringifying the object itself results in the value
819 from the item being displayed. Accessing attributes of this object
820 result in the appropriate entry from the class being queried for the
821 property accessed (so item/assignedto/name would look up the user
822 entry identified by the assignedto property on item, and then the
823 name property of that user)
824 '''
825 def __getattr__(self, attr):
826 ''' return a new HTMLItem '''
827 #print 'Link.getattr', (self, attr, self._value)
828 if not self._value:
829 raise AttributeError, "Can't access missing value"
830 if self._prop.classname == 'user':
831 klass = HTMLUser
832 else:
833 klass = HTMLItem
834 i = klass(self._client, self._prop.classname, self._value)
835 return getattr(i, attr)
837 def plain(self, escape=0):
838 ''' Render a "plain" representation of the property
839 '''
840 if self._value is None:
841 return ''
842 linkcl = self._db.classes[self._prop.classname]
843 k = linkcl.labelprop(1)
844 value = str(linkcl.get(self._value, k))
845 if escape:
846 value = cgi.escape(value)
847 return value
849 def field(self):
850 ''' Render a form edit field for the property
851 '''
852 linkcl = self._db.getclass(self._prop.classname)
853 if linkcl.getprops().has_key('order'):
854 sort_on = 'order'
855 else:
856 sort_on = linkcl.labelprop()
857 options = linkcl.filter(None, {}, [sort_on], [])
858 # TODO: make this a field display, not a menu one!
859 l = ['<select name="%s">'%property]
860 k = linkcl.labelprop(1)
861 if value is None:
862 s = 'selected '
863 else:
864 s = ''
865 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
866 for optionid in options:
867 option = linkcl.get(optionid, k)
868 s = ''
869 if optionid == value:
870 s = 'selected '
871 if showid:
872 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
873 else:
874 lab = option
875 if size is not None and len(lab) > size:
876 lab = lab[:size-3] + '...'
877 lab = cgi.escape(lab)
878 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
879 l.append('</select>')
880 return '\n'.join(l)
882 def menu(self, size=None, height=None, showid=0, additional=[],
883 **conditions):
884 value = self._value
886 # sort function
887 sortfunc = make_sort_function(self._db, self._prop.classname)
889 # force the value to be a single choice
890 if isinstance(value, type('')):
891 value = value[0]
892 linkcl = self._db.getclass(self._prop.classname)
893 l = ['<select name="%s">'%self._name]
894 k = linkcl.labelprop(1)
895 s = ''
896 if value is None:
897 s = 'selected '
898 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
899 if linkcl.getprops().has_key('order'):
900 sort_on = ('+', 'order')
901 else:
902 sort_on = ('+', linkcl.labelprop())
903 options = linkcl.filter(None, conditions, sort_on, (None, None))
904 for optionid in options:
905 option = linkcl.get(optionid, k)
906 s = ''
907 if value in [optionid, option]:
908 s = 'selected '
909 if showid:
910 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
911 else:
912 lab = option
913 if size is not None and len(lab) > size:
914 lab = lab[:size-3] + '...'
915 if additional:
916 m = []
917 for propname in additional:
918 m.append(linkcl.get(optionid, propname))
919 lab = lab + ' (%s)'%', '.join(map(str, m))
920 lab = cgi.escape(lab)
921 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
922 l.append('</select>')
923 return '\n'.join(l)
924 # def checklist(self, ...)
926 class MultilinkHTMLProperty(HTMLProperty):
927 ''' Multilink HTMLProperty
929 Also be iterable, returning a wrapper object like the Link case for
930 each entry in the multilink.
931 '''
932 def __len__(self):
933 ''' length of the multilink '''
934 return len(self._value)
936 def __getattr__(self, attr):
937 ''' no extended attribute accesses make sense here '''
938 raise AttributeError, attr
940 def __getitem__(self, num):
941 ''' iterate and return a new HTMLItem
942 '''
943 #print 'Multi.getitem', (self, num)
944 value = self._value[num]
945 if self._prop.classname == 'user':
946 klass = HTMLUser
947 else:
948 klass = HTMLItem
949 return klass(self._client, self._prop.classname, value)
951 def __contains__(self, value):
952 ''' Support the "in" operator
953 '''
954 return value in self._value
956 def reverse(self):
957 ''' return the list in reverse order
958 '''
959 l = self._value[:]
960 l.reverse()
961 if self._prop.classname == 'user':
962 klass = HTMLUser
963 else:
964 klass = HTMLItem
965 return [klass(self._client, self._prop.classname, value) for value in l]
967 def plain(self, escape=0):
968 ''' Render a "plain" representation of the property
969 '''
970 linkcl = self._db.classes[self._prop.classname]
971 k = linkcl.labelprop(1)
972 labels = []
973 for v in self._value:
974 labels.append(linkcl.get(v, k))
975 value = ', '.join(labels)
976 if escape:
977 value = cgi.escape(value)
978 return value
980 def field(self, size=30, showid=0):
981 ''' Render a form edit field for the property
982 '''
983 sortfunc = make_sort_function(self._db, self._prop.classname)
984 linkcl = self._db.getclass(self._prop.classname)
985 value = self._value[:]
986 if value:
987 value.sort(sortfunc)
988 # map the id to the label property
989 if not showid:
990 k = linkcl.labelprop(1)
991 value = [linkcl.get(v, k) for v in value]
992 value = cgi.escape(','.join(value))
993 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
995 def menu(self, size=None, height=None, showid=0, additional=[],
996 **conditions):
997 value = self._value
999 # sort function
1000 sortfunc = make_sort_function(self._db, self._prop.classname)
1002 linkcl = self._db.getclass(self._prop.classname)
1003 if linkcl.getprops().has_key('order'):
1004 sort_on = ('+', 'order')
1005 else:
1006 sort_on = ('+', linkcl.labelprop())
1007 options = linkcl.filter(None, conditions, sort_on, (None,None))
1008 height = height or min(len(options), 7)
1009 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1010 k = linkcl.labelprop(1)
1011 for optionid in options:
1012 option = linkcl.get(optionid, k)
1013 s = ''
1014 if optionid in value or option in value:
1015 s = 'selected '
1016 if showid:
1017 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1018 else:
1019 lab = option
1020 if size is not None and len(lab) > size:
1021 lab = lab[:size-3] + '...'
1022 if additional:
1023 m = []
1024 for propname in additional:
1025 m.append(linkcl.get(optionid, propname))
1026 lab = lab + ' (%s)'%', '.join(m)
1027 lab = cgi.escape(lab)
1028 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1029 lab))
1030 l.append('</select>')
1031 return '\n'.join(l)
1033 # set the propclasses for HTMLItem
1034 propclasses = (
1035 (hyperdb.String, StringHTMLProperty),
1036 (hyperdb.Number, NumberHTMLProperty),
1037 (hyperdb.Boolean, BooleanHTMLProperty),
1038 (hyperdb.Date, DateHTMLProperty),
1039 (hyperdb.Interval, IntervalHTMLProperty),
1040 (hyperdb.Password, PasswordHTMLProperty),
1041 (hyperdb.Link, LinkHTMLProperty),
1042 (hyperdb.Multilink, MultilinkHTMLProperty),
1043 )
1045 def make_sort_function(db, classname):
1046 '''Make a sort function for a given class
1047 '''
1048 linkcl = db.getclass(classname)
1049 if linkcl.getprops().has_key('order'):
1050 sort_on = 'order'
1051 else:
1052 sort_on = linkcl.labelprop()
1053 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1054 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1055 return sortfunc
1057 def handleListCGIValue(value):
1058 ''' Value is either a single item or a list of items. Each item has a
1059 .value that we're actually interested in.
1060 '''
1061 if isinstance(value, type([])):
1062 return [value.value for value in value]
1063 else:
1064 value = value.value.strip()
1065 if not value:
1066 return []
1067 return value.split(',')
1069 class ShowDict:
1070 ''' A convenience access to the :columns index parameters
1071 '''
1072 def __init__(self, columns):
1073 self.columns = {}
1074 for col in columns:
1075 self.columns[col] = 1
1076 def __getitem__(self, name):
1077 return self.columns.has_key(name)
1079 class HTMLRequest:
1080 ''' The *request*, holding the CGI form and environment.
1082 "form" the CGI form as a cgi.FieldStorage
1083 "env" the CGI environment variables
1084 "url" the current URL path for this request
1085 "base" the base URL for this instance
1086 "user" a HTMLUser instance for this user
1087 "classname" the current classname (possibly None)
1088 "template" the current template (suffix, also possibly None)
1090 Index args:
1091 "columns" dictionary of the columns to display in an index page
1092 "show" a convenience access to columns - request/show/colname will
1093 be true if the columns should be displayed, false otherwise
1094 "sort" index sort column (direction, column name)
1095 "group" index grouping property (direction, column name)
1096 "filter" properties to filter the index on
1097 "filterspec" values to filter the index on
1098 "search_text" text to perform a full-text search on for an index
1100 '''
1101 def __init__(self, client):
1102 self.client = client
1104 # easier access vars
1105 self.form = client.form
1106 self.env = client.env
1107 self.base = client.base
1108 self.url = client.url
1109 self.user = HTMLUser(client, 'user', client.userid)
1111 # store the current class name and action
1112 self.classname = client.classname
1113 self.template = client.template
1115 self._post_init()
1117 def _post_init(self):
1118 ''' Set attributes based on self.form
1119 '''
1120 # extract the index display information from the form
1121 self.columns = []
1122 if self.form.has_key(':columns'):
1123 self.columns = handleListCGIValue(self.form[':columns'])
1124 self.show = ShowDict(self.columns)
1126 # sorting
1127 self.sort = (None, None)
1128 if self.form.has_key(':sort'):
1129 sort = self.form[':sort'].value
1130 if sort.startswith('-'):
1131 self.sort = ('-', sort[1:])
1132 else:
1133 self.sort = ('+', sort)
1134 if self.form.has_key(':sortdir'):
1135 self.sort = ('-', self.sort[1])
1137 # grouping
1138 self.group = (None, None)
1139 if self.form.has_key(':group'):
1140 group = self.form[':group'].value
1141 if group.startswith('-'):
1142 self.group = ('-', group[1:])
1143 else:
1144 self.group = ('+', group)
1145 if self.form.has_key(':groupdir'):
1146 self.group = ('-', self.group[1])
1148 # filtering
1149 self.filter = []
1150 if self.form.has_key(':filter'):
1151 self.filter = handleListCGIValue(self.form[':filter'])
1152 self.filterspec = {}
1153 if self.classname is not None:
1154 props = self.client.db.getclass(self.classname).getprops()
1155 for name in self.filter:
1156 if self.form.has_key(name):
1157 prop = props[name]
1158 fv = self.form[name]
1159 if (isinstance(prop, hyperdb.Link) or
1160 isinstance(prop, hyperdb.Multilink)):
1161 self.filterspec[name] = handleListCGIValue(fv)
1162 else:
1163 self.filterspec[name] = fv.value
1165 # full-text search argument
1166 self.search_text = None
1167 if self.form.has_key(':search_text'):
1168 self.search_text = self.form[':search_text'].value
1170 # pagination - size and start index
1171 # figure batch args
1172 if self.form.has_key(':pagesize'):
1173 self.pagesize = int(self.form[':pagesize'].value)
1174 else:
1175 self.pagesize = 50
1176 if self.form.has_key(':startwith'):
1177 self.startwith = int(self.form[':startwith'].value)
1178 else:
1179 self.startwith = 0
1181 def updateFromURL(self, url):
1182 ''' Parse the URL for query args, and update my attributes using the
1183 values.
1184 '''
1185 self.form = {}
1186 for name, value in cgi.parse_qsl(url):
1187 if self.form.has_key(name):
1188 if isinstance(self.form[name], type([])):
1189 self.form[name].append(cgi.MiniFieldStorage(name, value))
1190 else:
1191 self.form[name] = [self.form[name],
1192 cgi.MiniFieldStorage(name, value)]
1193 else:
1194 self.form[name] = cgi.MiniFieldStorage(name, value)
1195 self._post_init()
1197 def update(self, kwargs):
1198 ''' Update my attributes using the keyword args
1199 '''
1200 self.__dict__.update(kwargs)
1201 if kwargs.has_key('columns'):
1202 self.show = ShowDict(self.columns)
1204 def description(self):
1205 ''' Return a description of the request - handle for the page title.
1206 '''
1207 s = [self.client.db.config.TRACKER_NAME]
1208 if self.classname:
1209 if self.client.nodeid:
1210 s.append('- %s%s'%(self.classname, self.client.nodeid))
1211 else:
1212 if self.template == 'item':
1213 s.append('- new %s'%self.classname)
1214 elif self.template == 'index':
1215 s.append('- %s index'%self.classname)
1216 else:
1217 s.append('- %s %s'%(self.classname, self.template))
1218 else:
1219 s.append('- home')
1220 return ' '.join(s)
1222 def __str__(self):
1223 d = {}
1224 d.update(self.__dict__)
1225 f = ''
1226 for k in self.form.keys():
1227 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1228 d['form'] = f
1229 e = ''
1230 for k,v in self.env.items():
1231 e += '\n %r=%r'%(k, v)
1232 d['env'] = e
1233 return '''
1234 form: %(form)s
1235 url: %(url)r
1236 base: %(base)r
1237 classname: %(classname)r
1238 template: %(template)r
1239 columns: %(columns)r
1240 sort: %(sort)r
1241 group: %(group)r
1242 filter: %(filter)r
1243 search_text: %(search_text)r
1244 pagesize: %(pagesize)r
1245 startwith: %(startwith)r
1246 env: %(env)s
1247 '''%d
1249 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1250 filterspec=1):
1251 ''' return the current index args as form elements '''
1252 l = []
1253 s = '<input type="hidden" name="%s" value="%s">'
1254 if columns and self.columns:
1255 l.append(s%(':columns', ','.join(self.columns)))
1256 if sort and self.sort[1] is not None:
1257 if self.sort[0] == '-':
1258 val = '-'+self.sort[1]
1259 else:
1260 val = self.sort[1]
1261 l.append(s%(':sort', val))
1262 if group and self.group[1] is not None:
1263 if self.group[0] == '-':
1264 val = '-'+self.group[1]
1265 else:
1266 val = self.group[1]
1267 l.append(s%(':group', val))
1268 if filter and self.filter:
1269 l.append(s%(':filter', ','.join(self.filter)))
1270 if filterspec:
1271 for k,v in self.filterspec.items():
1272 l.append(s%(k, ','.join(v)))
1273 if self.search_text:
1274 l.append(s%(':search_text', self.search_text))
1275 l.append(s%(':pagesize', self.pagesize))
1276 l.append(s%(':startwith', self.startwith))
1277 return '\n'.join(l)
1279 def indexargs_href(self, url, args):
1280 ''' embed the current index args in a URL '''
1281 l = ['%s=%s'%(k,v) for k,v in args.items()]
1282 if self.columns and not args.has_key(':columns'):
1283 l.append(':columns=%s'%(','.join(self.columns)))
1284 if self.sort[1] is not None and not args.has_key(':sort'):
1285 if self.sort[0] == '-':
1286 val = '-'+self.sort[1]
1287 else:
1288 val = self.sort[1]
1289 l.append(':sort=%s'%val)
1290 if self.group[1] is not None and not args.has_key(':group'):
1291 if self.group[0] == '-':
1292 val = '-'+self.group[1]
1293 else:
1294 val = self.group[1]
1295 l.append(':group=%s'%val)
1296 if self.filter and not args.has_key(':columns'):
1297 l.append(':filter=%s'%(','.join(self.filter)))
1298 for k,v in self.filterspec.items():
1299 if not args.has_key(k):
1300 l.append('%s=%s'%(k, ','.join(v)))
1301 if self.search_text and not args.has_key(':search_text'):
1302 l.append(':search_text=%s'%self.search_text)
1303 if not args.has_key(':pagesize'):
1304 l.append(':pagesize=%s'%self.pagesize)
1305 if not args.has_key(':startwith'):
1306 l.append(':startwith=%s'%self.startwith)
1307 return '%s?%s'%(url, '&'.join(l))
1309 def base_javascript(self):
1310 return '''
1311 <script language="javascript">
1312 submitted = false;
1313 function submit_once() {
1314 if (submitted) {
1315 alert("Your request is being processed.\\nPlease be patient.");
1316 return 0;
1317 }
1318 submitted = true;
1319 return 1;
1320 }
1322 function help_window(helpurl, width, height) {
1323 HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1324 }
1325 </script>
1326 '''%self.base
1328 def batch(self):
1329 ''' Return a batch object for results from the "current search"
1330 '''
1331 filterspec = self.filterspec
1332 sort = self.sort
1333 group = self.group
1335 # get the list of ids we're batching over
1336 klass = self.client.db.getclass(self.classname)
1337 if self.search_text:
1338 matches = self.client.db.indexer.search(
1339 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1340 else:
1341 matches = None
1342 l = klass.filter(matches, filterspec, sort, group)
1344 # map the item ids to instances
1345 if self.classname == 'user':
1346 klass = HTMLUser
1347 else:
1348 klass = HTMLItem
1349 l = [klass(self.client, self.classname, item) for item in l]
1351 # return the batch object
1352 return Batch(self.client, l, self.pagesize, self.startwith)
1354 # extend the standard ZTUtils Batch object to remove dependency on
1355 # Acquisition and add a couple of useful methods
1356 class Batch(ZTUtils.Batch):
1357 ''' Use me to turn a list of items, or item ids of a given class, into a
1358 series of batches.
1360 ========= ========================================================
1361 Parameter Usage
1362 ========= ========================================================
1363 sequence a list of HTMLItems
1364 size how big to make the sequence.
1365 start where to start (0-indexed) in the sequence.
1366 end where to end (0-indexed) in the sequence.
1367 orphan if the next batch would contain less items than this
1368 value, then it is combined with this batch
1369 overlap the number of items shared between adjacent batches
1370 ========= ========================================================
1372 Attributes: Note that the "start" attribute, unlike the
1373 argument, is a 1-based index (I know, lame). "first" is the
1374 0-based index. "length" is the actual number of elements in
1375 the batch.
1377 "sequence_length" is the length of the original, unbatched, sequence.
1378 '''
1379 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1380 overlap=0):
1381 self.client = client
1382 self.last_index = self.last_item = None
1383 self.current_item = None
1384 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1385 overlap)
1387 # overwrite so we can late-instantiate the HTMLItem instance
1388 def __getitem__(self, index):
1389 if index < 0:
1390 if index + self.end < self.first: raise IndexError, index
1391 return self._sequence[index + self.end]
1393 if index >= self.length:
1394 raise IndexError, index
1396 # move the last_item along - but only if the fetched index changes
1397 # (for some reason, index 0 is fetched twice)
1398 if index != self.last_index:
1399 self.last_item = self.current_item
1400 self.last_index = index
1402 self.current_item = self._sequence[index + self.first]
1403 return self.current_item
1405 def propchanged(self, property):
1406 ''' Detect if the property marked as being the group property
1407 changed in the last iteration fetch
1408 '''
1409 if (self.last_item is None or
1410 self.last_item[property] != self.current_item[property]):
1411 return 1
1412 return 0
1414 # override these 'cos we don't have access to acquisition
1415 def previous(self):
1416 if self.start == 1:
1417 return None
1418 return Batch(self.client, self._sequence, self._size,
1419 self.first - self._size + self.overlap, 0, self.orphan,
1420 self.overlap)
1422 def next(self):
1423 try:
1424 self._sequence[self.end]
1425 except IndexError:
1426 return None
1427 return Batch(self.client, self._sequence, self._size,
1428 self.end - self.overlap, 0, self.orphan, self.overlap)
1430 def length(self):
1431 self.sequence_length = l = len(self._sequence)
1432 return l
1434 class TemplatingUtils:
1435 ''' Utilities for templating
1436 '''
1437 def __init__(self, client):
1438 self.client = client
1439 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1440 return Batch(self.client, sequence, size, start, end, orphan,
1441 overlap)