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