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 }
155 # add in the item if there is one
156 if client.nodeid:
157 c['context'] = HTMLItem(client, classname, client.nodeid)
158 else:
159 c['context'] = HTMLClass(client, classname)
160 return c
162 def render(self, client, classname, request, **options):
163 """Render this Page Template"""
165 if not self._v_cooked:
166 self._cook()
168 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
170 if self._v_errors:
171 raise PageTemplate.PTRuntimeError, \
172 'Page Template %s has errors.' % self.id
174 # figure the context
175 classname = classname or client.classname
176 request = request or HTMLRequest(client)
177 c = self.getContext(client, classname, request)
178 c.update({'options': options})
180 # and go
181 output = StringIO.StringIO()
182 TALInterpreter(self._v_program, self._v_macros,
183 getEngine().getContext(c), output, tal=1, strictinsert=0)()
184 return output.getvalue()
186 class HTMLDatabase:
187 ''' Return HTMLClasses for valid class fetches
188 '''
189 def __init__(self, client):
190 self._client = client
192 # we want config to be exposed
193 self.config = client.db.config
195 def __getattr__(self, attr):
196 try:
197 self._client.db.getclass(attr)
198 except KeyError:
199 raise AttributeError, attr
200 return HTMLClass(self._client, attr)
201 def classes(self):
202 l = self._client.db.classes.keys()
203 l.sort()
204 return [HTMLClass(self._client, cn) for cn in l]
206 class HTMLClass:
207 ''' Accesses through a class (either through *class* or *db.<classname>*)
208 '''
209 def __init__(self, client, classname):
210 self._client = client
211 self._db = client.db
213 # we want classname to be exposed
214 self.classname = classname
215 if classname is not None:
216 self._klass = self._db.getclass(self.classname)
217 self._props = self._klass.getprops()
219 def __repr__(self):
220 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
222 def __getitem__(self, item):
223 ''' return an HTMLProperty instance
224 '''
225 #print 'getitem', (self, item)
227 # we don't exist
228 if item == 'id':
229 return None
231 # get the property
232 prop = self._props[item]
234 # look up the correct HTMLProperty class
235 for klass, htmlklass in propclasses:
236 if isinstance(prop, hyperdb.Multilink):
237 value = []
238 else:
239 value = None
240 if isinstance(prop, klass):
241 return htmlklass(self._client, '', prop, item, value)
243 # no good
244 raise KeyError, item
246 def __getattr__(self, attr):
247 ''' convenience access '''
248 try:
249 return self[attr]
250 except KeyError:
251 raise AttributeError, attr
253 def properties(self):
254 ''' Return HTMLProperty for all props
255 '''
256 l = []
257 for name, prop in self._props.items():
258 for klass, htmlklass in propclasses:
259 if isinstance(prop, hyperdb.Multilink):
260 value = []
261 else:
262 value = None
263 if isinstance(prop, klass):
264 l.append(htmlklass(self._client, '', prop, name, value))
265 return l
267 def list(self):
268 if self.classname == 'user':
269 klass = HTMLUser
270 else:
271 klass = HTMLItem
272 l = [klass(self._client, self.classname, x) for x in self._klass.list()]
273 return l
275 def csv(self):
276 ''' Return the items of this class as a chunk of CSV text.
277 '''
278 # get the CSV module
279 try:
280 import csv
281 except ImportError:
282 return 'Sorry, you need the csv module to use this function.\n'\
283 'Get it from: http://www.object-craft.com.au/projects/csv/'
285 props = self.propnames()
286 p = csv.parser()
287 s = StringIO.StringIO()
288 s.write(p.join(props) + '\n')
289 for nodeid in self._klass.list():
290 l = []
291 for name in props:
292 value = self._klass.get(nodeid, name)
293 if value is None:
294 l.append('')
295 elif isinstance(value, type([])):
296 l.append(':'.join(map(str, value)))
297 else:
298 l.append(str(self._klass.get(nodeid, name)))
299 s.write(p.join(l) + '\n')
300 return s.getvalue()
302 def propnames(self):
303 ''' Return the list of the names of the properties of this class.
304 '''
305 idlessprops = self._klass.getprops(protected=0).keys()
306 idlessprops.sort()
307 return ['id'] + idlessprops
309 def filter(self, request=None):
310 ''' Return a list of items from this class, filtered and sorted
311 by the current requested filterspec/filter/sort/group args
312 '''
313 if request is not None:
314 filterspec = request.filterspec
315 sort = request.sort
316 group = request.group
317 if self.classname == 'user':
318 klass = HTMLUser
319 else:
320 klass = HTMLItem
321 l = [klass(self._client, self.classname, x)
322 for x in self._klass.filter(None, filterspec, sort, group)]
323 return l
325 def classhelp(self, properties, label='?', width='400', height='400'):
326 '''pop up a javascript window with class help
328 This generates a link to a popup window which displays the
329 properties indicated by "properties" of the class named by
330 "classname". The "properties" should be a comma-separated list
331 (eg. 'id,name,description').
333 You may optionally override the label displayed, the width and
334 height. The popup window will be resizable and scrollable.
335 '''
336 return '<a href="javascript:help_window(\'%s?:template=help&' \
337 ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
338 '(%s)</b></a>'%(self.classname, properties, width, height, label)
340 def submit(self, label="Submit New Entry"):
341 ''' Generate a submit button (and action hidden element)
342 '''
343 return ' <input type="hidden" name=":action" value="new">\n'\
344 ' <input type="submit" name="submit" value="%s">'%label
346 def history(self):
347 return 'New node - no history'
349 def renderWith(self, name, **kwargs):
350 ''' Render this class with the given template.
351 '''
352 # create a new request and override the specified args
353 req = HTMLRequest(self._client)
354 req.classname = self.classname
355 req.update(kwargs)
357 # new template, using the specified classname and request
358 pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
360 # use our fabricated request
361 return pt.render(self._client, self.classname, req)
363 class HTMLItem:
364 ''' Accesses through an *item*
365 '''
366 def __init__(self, client, classname, nodeid):
367 self._client = client
368 self._db = client.db
369 self._classname = classname
370 self._nodeid = nodeid
371 self._klass = self._db.getclass(classname)
372 self._props = self._klass.getprops()
374 def __repr__(self):
375 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
376 self._nodeid)
378 def __getitem__(self, item):
379 ''' return an HTMLProperty instance
380 '''
381 #print 'getitem', (self, item)
382 if item == 'id':
383 return self._nodeid
385 # get the property
386 prop = self._props[item]
388 # get the value, handling missing values
389 value = self._klass.get(self._nodeid, item, None)
390 if value is None:
391 if isinstance(self._props[item], hyperdb.Multilink):
392 value = []
394 # look up the correct HTMLProperty class
395 for klass, htmlklass in propclasses:
396 if isinstance(prop, klass):
397 return htmlklass(self._client, self._nodeid, prop, item, value)
399 raise KeyErorr, item
401 def __getattr__(self, attr):
402 ''' convenience access to properties '''
403 try:
404 return self[attr]
405 except KeyError:
406 raise AttributeError, attr
408 def submit(self, label="Submit Changes"):
409 ''' Generate a submit button (and action hidden element)
410 '''
411 return ' <input type="hidden" name=":action" value="edit">\n'\
412 ' <input type="submit" name="submit" value="%s">'%label
414 # XXX this probably should just return the history items, not the HTML
415 def history(self, direction='descending'):
416 l = ['<table class="history">'
417 '<tr><th colspan="4" class="header">',
418 _('History'),
419 '</th></tr><tr>',
420 _('<th>Date</th>'),
421 _('<th>User</th>'),
422 _('<th>Action</th>'),
423 _('<th>Args</th>'),
424 '</tr>']
425 comments = {}
426 history = self._klass.history(self._nodeid)
427 history.sort()
428 if direction == 'descending':
429 history.reverse()
430 for id, evt_date, user, action, args in history:
431 date_s = str(evt_date).replace("."," ")
432 arg_s = ''
433 if action == 'link' and type(args) == type(()):
434 if len(args) == 3:
435 linkcl, linkid, key = args
436 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
437 linkcl, linkid, key)
438 else:
439 arg_s = str(args)
441 elif action == 'unlink' and type(args) == type(()):
442 if len(args) == 3:
443 linkcl, linkid, key = args
444 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
445 linkcl, linkid, key)
446 else:
447 arg_s = str(args)
449 elif type(args) == type({}):
450 cell = []
451 for k in args.keys():
452 # try to get the relevant property and treat it
453 # specially
454 try:
455 prop = self._props[k]
456 except KeyError:
457 prop = None
458 if prop is not None:
459 if args[k] and (isinstance(prop, hyperdb.Multilink) or
460 isinstance(prop, hyperdb.Link)):
461 # figure what the link class is
462 classname = prop.classname
463 try:
464 linkcl = self._db.getclass(classname)
465 except KeyError:
466 labelprop = None
467 comments[classname] = _('''The linked class
468 %(classname)s no longer exists''')%locals()
469 labelprop = linkcl.labelprop(1)
470 hrefable = os.path.exists(
471 os.path.join(self._db.config.TEMPLATES,
472 classname+'.item'))
474 if isinstance(prop, hyperdb.Multilink) and \
475 len(args[k]) > 0:
476 ml = []
477 for linkid in args[k]:
478 if isinstance(linkid, type(())):
479 sublabel = linkid[0] + ' '
480 linkids = linkid[1]
481 else:
482 sublabel = ''
483 linkids = [linkid]
484 subml = []
485 for linkid in linkids:
486 label = classname + linkid
487 # if we have a label property, try to use it
488 # TODO: test for node existence even when
489 # there's no labelprop!
490 try:
491 if labelprop is not None:
492 label = linkcl.get(linkid, labelprop)
493 except IndexError:
494 comments['no_link'] = _('''<strike>The
495 linked node no longer
496 exists</strike>''')
497 subml.append('<strike>%s</strike>'%label)
498 else:
499 if hrefable:
500 subml.append('<a href="%s%s">%s</a>'%(
501 classname, linkid, label))
502 ml.append(sublabel + ', '.join(subml))
503 cell.append('%s:\n %s'%(k, ', '.join(ml)))
504 elif isinstance(prop, hyperdb.Link) and args[k]:
505 label = classname + args[k]
506 # if we have a label property, try to use it
507 # TODO: test for node existence even when
508 # there's no labelprop!
509 if labelprop is not None:
510 try:
511 label = linkcl.get(args[k], labelprop)
512 except IndexError:
513 comments['no_link'] = _('''<strike>The
514 linked node no longer
515 exists</strike>''')
516 cell.append(' <strike>%s</strike>,\n'%label)
517 # "flag" this is done .... euwww
518 label = None
519 if label is not None:
520 if hrefable:
521 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
522 classname, args[k], label))
523 else:
524 cell.append('%s: %s' % (k,label))
526 elif isinstance(prop, hyperdb.Date) and args[k]:
527 d = date.Date(args[k])
528 cell.append('%s: %s'%(k, str(d)))
530 elif isinstance(prop, hyperdb.Interval) and args[k]:
531 d = date.Interval(args[k])
532 cell.append('%s: %s'%(k, str(d)))
534 elif isinstance(prop, hyperdb.String) and args[k]:
535 cell.append('%s: %s'%(k, cgi.escape(args[k])))
537 elif not args[k]:
538 cell.append('%s: (no value)\n'%k)
540 else:
541 cell.append('%s: %s\n'%(k, str(args[k])))
542 else:
543 # property no longer exists
544 comments['no_exist'] = _('''<em>The indicated property
545 no longer exists</em>''')
546 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
547 arg_s = '<br />'.join(cell)
548 else:
549 # unkown event!!
550 comments['unknown'] = _('''<strong><em>This event is not
551 handled by the history display!</em></strong>''')
552 arg_s = '<strong><em>' + str(args) + '</em></strong>'
553 date_s = date_s.replace(' ', ' ')
554 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
555 date_s, user, action, arg_s))
556 if comments:
557 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
558 for entry in comments.values():
559 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
560 l.append('</table>')
561 return '\n'.join(l)
563 def renderQueryForm(self):
564 ''' Render this item, which is a query, as a search form.
565 '''
566 # create a new request and override the specified args
567 req = HTMLRequest(self._client)
568 req.classname = self._klass.get(self._nodeid, 'klass')
569 req.updateFromURL(self._klass.get(self._nodeid, 'url'))
571 # new template, using the specified classname and request
572 pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
574 # use our fabricated request
575 return pt.render(self._client, req.classname, req)
577 class HTMLUser(HTMLItem):
578 ''' Accesses through the *user* (a special case of item)
579 '''
580 def __init__(self, client, classname, nodeid):
581 HTMLItem.__init__(self, client, 'user', nodeid)
582 self._default_classname = client.classname
584 # used for security checks
585 self._security = client.db.security
586 _marker = []
587 def hasPermission(self, role, classname=_marker):
588 ''' Determine if the user has the Role.
590 The class being tested defaults to the template's class, but may
591 be overidden for this test by suppling an alternate classname.
592 '''
593 if classname is self._marker:
594 classname = self._default_classname
595 return self._security.hasPermission(role, self._nodeid, classname)
597 class HTMLProperty:
598 ''' String, Number, Date, Interval HTMLProperty
600 Hase useful attributes:
602 _name the name of the property
603 _value the value of the property if any
605 A wrapper object which may be stringified for the plain() behaviour.
606 '''
607 def __init__(self, client, nodeid, prop, name, value):
608 self._client = client
609 self._db = client.db
610 self._nodeid = nodeid
611 self._prop = prop
612 self._name = name
613 self._value = value
614 def __repr__(self):
615 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
616 def __str__(self):
617 return self.plain()
618 def __cmp__(self, other):
619 if isinstance(other, HTMLProperty):
620 return cmp(self._value, other._value)
621 return cmp(self._value, other)
623 class StringHTMLProperty(HTMLProperty):
624 def plain(self, escape=0):
625 if self._value is None:
626 return ''
627 if escape:
628 return cgi.escape(str(self._value))
629 return str(self._value)
631 def stext(self, escape=0):
632 s = self.plain(escape=escape)
633 if not StructuredText:
634 return s
635 return StructuredText(s,level=1,header=0)
637 def field(self, size = 30):
638 if self._value is None:
639 value = ''
640 else:
641 value = cgi.escape(str(self._value))
642 value = '"'.join(value.split('"'))
643 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
645 def multiline(self, escape=0, rows=5, cols=40):
646 if self._value is None:
647 value = ''
648 else:
649 value = cgi.escape(str(self._value))
650 value = '"'.join(value.split('"'))
651 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
652 self._name, rows, cols, value)
654 def email(self, escape=1):
655 ''' fudge email '''
656 if self._value is None: value = ''
657 else: value = str(self._value)
658 value = value.replace('@', ' at ')
659 value = value.replace('.', ' ')
660 if escape:
661 value = cgi.escape(value)
662 return value
664 class PasswordHTMLProperty(HTMLProperty):
665 def plain(self):
666 if self._value is None:
667 return ''
668 return _('*encrypted*')
670 def field(self, size = 30):
671 return '<input type="password" name="%s" size="%s">'%(self._name, size)
673 class NumberHTMLProperty(HTMLProperty):
674 def plain(self):
675 return str(self._value)
677 def field(self, size = 30):
678 if self._value is None:
679 value = ''
680 else:
681 value = cgi.escape(str(self._value))
682 value = '"'.join(value.split('"'))
683 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
685 class BooleanHTMLProperty(HTMLProperty):
686 def plain(self):
687 if self.value is None:
688 return ''
689 return self._value and "Yes" or "No"
691 def field(self):
692 checked = self._value and "checked" or ""
693 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
694 checked)
695 if checked:
696 checked = ""
697 else:
698 checked = "checked"
699 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
700 checked)
701 return s
703 class DateHTMLProperty(HTMLProperty):
704 def plain(self):
705 if self._value is None:
706 return ''
707 return str(self._value)
709 def field(self, size = 30):
710 if self._value is None:
711 value = ''
712 else:
713 value = cgi.escape(str(self._value))
714 value = '"'.join(value.split('"'))
715 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
717 def reldate(self, pretty=1):
718 if not self._value:
719 return ''
721 # figure the interval
722 interval = date.Date('.') - self._value
723 if pretty:
724 return interval.pretty()
725 return str(interval)
727 class IntervalHTMLProperty(HTMLProperty):
728 def plain(self):
729 if self._value is None:
730 return ''
731 return str(self._value)
733 def pretty(self):
734 return self._value.pretty()
736 def field(self, size = 30):
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 LinkHTMLProperty(HTMLProperty):
745 ''' Link HTMLProperty
746 Include the above as well as being able to access the class
747 information. Stringifying the object itself results in the value
748 from the item being displayed. Accessing attributes of this object
749 result in the appropriate entry from the class being queried for the
750 property accessed (so item/assignedto/name would look up the user
751 entry identified by the assignedto property on item, and then the
752 name property of that user)
753 '''
754 def __getattr__(self, attr):
755 ''' return a new HTMLItem '''
756 #print 'getattr', (self, attr, self._value)
757 if not self._value:
758 raise AttributeError, "Can't access missing value"
759 if self._prop.classname == 'user':
760 klass = HTMLItem
761 else:
762 klass = HTMLUser
763 i = klass(self._client, self._prop.classname, self._value)
764 return getattr(i, attr)
766 def plain(self, escape=0):
767 if self._value is None:
768 return ''
769 linkcl = self._db.classes[self._prop.classname]
770 k = linkcl.labelprop(1)
771 value = str(linkcl.get(self._value, k))
772 if escape:
773 value = cgi.escape(value)
774 return value
776 def field(self):
777 linkcl = self._db.getclass(self._prop.classname)
778 if linkcl.getprops().has_key('order'):
779 sort_on = 'order'
780 else:
781 sort_on = linkcl.labelprop()
782 options = linkcl.filter(None, {}, [sort_on], [])
783 # TODO: make this a field display, not a menu one!
784 l = ['<select name="%s">'%property]
785 k = linkcl.labelprop(1)
786 if value is None:
787 s = 'selected '
788 else:
789 s = ''
790 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
791 for optionid in options:
792 option = linkcl.get(optionid, k)
793 s = ''
794 if optionid == value:
795 s = 'selected '
796 if showid:
797 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
798 else:
799 lab = option
800 if size is not None and len(lab) > size:
801 lab = lab[:size-3] + '...'
802 lab = cgi.escape(lab)
803 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
804 l.append('</select>')
805 return '\n'.join(l)
807 def menu(self, size=None, height=None, showid=0, additional=[],
808 **conditions):
809 value = self._value
811 # sort function
812 sortfunc = make_sort_function(self._db, self._prop.classname)
814 # force the value to be a single choice
815 if isinstance(value, type('')):
816 value = value[0]
817 linkcl = self._db.getclass(self._prop.classname)
818 l = ['<select name="%s">'%self._name]
819 k = linkcl.labelprop(1)
820 s = ''
821 if value is None:
822 s = 'selected '
823 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
824 if linkcl.getprops().has_key('order'):
825 sort_on = ('+', 'order')
826 else:
827 sort_on = ('+', linkcl.labelprop())
828 options = linkcl.filter(None, conditions, sort_on, (None, None))
829 for optionid in options:
830 option = linkcl.get(optionid, k)
831 s = ''
832 if value in [optionid, option]:
833 s = 'selected '
834 if showid:
835 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
836 else:
837 lab = option
838 if size is not None and len(lab) > size:
839 lab = lab[:size-3] + '...'
840 if additional:
841 m = []
842 for propname in additional:
843 m.append(linkcl.get(optionid, propname))
844 lab = lab + ' (%s)'%', '.join(map(str, m))
845 lab = cgi.escape(lab)
846 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
847 l.append('</select>')
848 return '\n'.join(l)
849 # def checklist(self, ...)
851 class MultilinkHTMLProperty(HTMLProperty):
852 ''' Multilink HTMLProperty
854 Also be iterable, returning a wrapper object like the Link case for
855 each entry in the multilink.
856 '''
857 def __len__(self):
858 ''' length of the multilink '''
859 return len(self._value)
861 def __getattr__(self, attr):
862 ''' no extended attribute accesses make sense here '''
863 raise AttributeError, attr
865 def __getitem__(self, num):
866 ''' iterate and return a new HTMLItem
867 '''
868 #print 'getitem', (self, num)
869 value = self._value[num]
870 if self._prop.classname == 'user':
871 klass = HTMLUser
872 else:
873 klass = HTMLItem
874 return klass(self._client, self._prop.classname, value)
876 def reverse(self):
877 ''' return the list in reverse order
878 '''
879 l = self._value[:]
880 l.reverse()
881 if self._prop.classname == 'user':
882 klass = HTMLUser
883 else:
884 klass = HTMLItem
885 return [klass(self._client, self._prop.classname, value) for value in l]
887 def plain(self, escape=0):
888 linkcl = self._db.classes[self._prop.classname]
889 k = linkcl.labelprop(1)
890 labels = []
891 for v in self._value:
892 labels.append(linkcl.get(v, k))
893 value = ', '.join(labels)
894 if escape:
895 value = cgi.escape(value)
896 return value
898 def field(self, size=30, showid=0):
899 sortfunc = make_sort_function(self._db, self._prop.classname)
900 linkcl = self._db.getclass(self._prop.classname)
901 value = self._value[:]
902 if value:
903 value.sort(sortfunc)
904 # map the id to the label property
905 if not showid:
906 k = linkcl.labelprop(1)
907 value = [linkcl.get(v, k) for v in value]
908 value = cgi.escape(','.join(value))
909 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
911 def menu(self, size=None, height=None, showid=0, additional=[],
912 **conditions):
913 value = self._value
915 # sort function
916 sortfunc = make_sort_function(self._db, self._prop.classname)
918 linkcl = self._db.getclass(self._prop.classname)
919 if linkcl.getprops().has_key('order'):
920 sort_on = ('+', 'order')
921 else:
922 sort_on = ('+', linkcl.labelprop())
923 options = linkcl.filter(None, conditions, sort_on, (None,None))
924 height = height or min(len(options), 7)
925 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
926 k = linkcl.labelprop(1)
927 for optionid in options:
928 option = linkcl.get(optionid, k)
929 s = ''
930 if optionid in value or option in value:
931 s = 'selected '
932 if showid:
933 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
934 else:
935 lab = option
936 if size is not None and len(lab) > size:
937 lab = lab[:size-3] + '...'
938 if additional:
939 m = []
940 for propname in additional:
941 m.append(linkcl.get(optionid, propname))
942 lab = lab + ' (%s)'%', '.join(m)
943 lab = cgi.escape(lab)
944 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
945 lab))
946 l.append('</select>')
947 return '\n'.join(l)
949 # set the propclasses for HTMLItem
950 propclasses = (
951 (hyperdb.String, StringHTMLProperty),
952 (hyperdb.Number, NumberHTMLProperty),
953 (hyperdb.Boolean, BooleanHTMLProperty),
954 (hyperdb.Date, DateHTMLProperty),
955 (hyperdb.Interval, IntervalHTMLProperty),
956 (hyperdb.Password, PasswordHTMLProperty),
957 (hyperdb.Link, LinkHTMLProperty),
958 (hyperdb.Multilink, MultilinkHTMLProperty),
959 )
961 def make_sort_function(db, classname):
962 '''Make a sort function for a given class
963 '''
964 linkcl = db.getclass(classname)
965 if linkcl.getprops().has_key('order'):
966 sort_on = 'order'
967 else:
968 sort_on = linkcl.labelprop()
969 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
970 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
971 return sortfunc
973 def handleListCGIValue(value):
974 ''' Value is either a single item or a list of items. Each item has a
975 .value that we're actually interested in.
976 '''
977 if isinstance(value, type([])):
978 return [value.value for value in value]
979 else:
980 return value.value.split(',')
982 class ShowDict:
983 ''' A convenience access to the :columns index parameters
984 '''
985 def __init__(self, columns):
986 self.columns = {}
987 for col in columns:
988 self.columns[col] = 1
989 def __getitem__(self, name):
990 return self.columns.has_key(name)
992 class HTMLRequest:
993 ''' The *request*, holding the CGI form and environment.
995 "form" the CGI form as a cgi.FieldStorage
996 "env" the CGI environment variables
997 "url" the current URL path for this request
998 "base" the base URL for this instance
999 "user" a HTMLUser instance for this user
1000 "classname" the current classname (possibly None)
1001 "template" the current template (suffix, also possibly None)
1003 Index args:
1004 "columns" dictionary of the columns to display in an index page
1005 "show" a convenience access to columns - request/show/colname will
1006 be true if the columns should be displayed, false otherwise
1007 "sort" index sort column (direction, column name)
1008 "group" index grouping property (direction, column name)
1009 "filter" properties to filter the index on
1010 "filterspec" values to filter the index on
1011 "search_text" text to perform a full-text search on for an index
1013 '''
1014 def __init__(self, client):
1015 self.client = client
1017 # easier access vars
1018 self.form = client.form
1019 self.env = client.env
1020 self.base = client.base
1021 self.url = client.url
1022 self.user = HTMLUser(client, 'user', client.userid)
1024 # store the current class name and action
1025 self.classname = client.classname
1026 self.template = client.template
1028 self._post_init()
1030 def _post_init(self):
1031 ''' Set attributes based on self.form
1032 '''
1033 # extract the index display information from the form
1034 self.columns = []
1035 if self.form.has_key(':columns'):
1036 self.columns = handleListCGIValue(self.form[':columns'])
1037 self.show = ShowDict(self.columns)
1039 # sorting
1040 self.sort = (None, None)
1041 if self.form.has_key(':sort'):
1042 sort = self.form[':sort'].value
1043 if sort.startswith('-'):
1044 self.sort = ('-', sort[1:])
1045 else:
1046 self.sort = ('+', sort)
1047 if self.form.has_key(':sortdir'):
1048 self.sort = ('-', self.sort[1])
1050 # grouping
1051 self.group = (None, None)
1052 if self.form.has_key(':group'):
1053 group = self.form[':group'].value
1054 if group.startswith('-'):
1055 self.group = ('-', group[1:])
1056 else:
1057 self.group = ('+', group)
1058 if self.form.has_key(':groupdir'):
1059 self.group = ('-', self.group[1])
1061 # filtering
1062 self.filter = []
1063 if self.form.has_key(':filter'):
1064 self.filter = handleListCGIValue(self.form[':filter'])
1065 self.filterspec = {}
1066 if self.classname is not None:
1067 props = self.client.db.getclass(self.classname).getprops()
1068 for name in self.filter:
1069 if self.form.has_key(name):
1070 prop = props[name]
1071 fv = self.form[name]
1072 if (isinstance(prop, hyperdb.Link) or
1073 isinstance(prop, hyperdb.Multilink)):
1074 self.filterspec[name] = handleListCGIValue(fv)
1075 else:
1076 self.filterspec[name] = fv.value
1078 # full-text search argument
1079 self.search_text = None
1080 if self.form.has_key(':search_text'):
1081 self.search_text = self.form[':search_text'].value
1083 # pagination - size and start index
1084 # figure batch args
1085 if self.form.has_key(':pagesize'):
1086 self.pagesize = int(self.form[':pagesize'].value)
1087 else:
1088 self.pagesize = 50
1089 if self.form.has_key(':startwith'):
1090 self.startwith = int(self.form[':startwith'].value)
1091 else:
1092 self.startwith = 0
1094 def updateFromURL(self, url):
1095 ''' Parse the URL for query args, and update my attributes using the
1096 values.
1097 '''
1098 self.form = {}
1099 for name, value in cgi.parse_qsl(url):
1100 if self.form.has_key(name):
1101 if isinstance(self.form[name], type([])):
1102 self.form[name].append(cgi.MiniFieldStorage(name, value))
1103 else:
1104 self.form[name] = [self.form[name],
1105 cgi.MiniFieldStorage(name, value)]
1106 else:
1107 self.form[name] = cgi.MiniFieldStorage(name, value)
1108 self._post_init()
1110 def update(self, kwargs):
1111 ''' Update my attributes using the keyword args
1112 '''
1113 self.__dict__.update(kwargs)
1114 if kwargs.has_key('columns'):
1115 self.show = ShowDict(self.columns)
1117 def description(self):
1118 ''' Return a description of the request - handle for the page title.
1119 '''
1120 s = [self.client.db.config.TRACKER_NAME]
1121 if self.classname:
1122 if self.client.nodeid:
1123 s.append('- %s%s'%(self.classname, self.client.nodeid))
1124 else:
1125 s.append('- index of '+self.classname)
1126 else:
1127 s.append('- home')
1128 return ' '.join(s)
1130 def __str__(self):
1131 d = {}
1132 d.update(self.__dict__)
1133 f = ''
1134 for k in self.form.keys():
1135 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1136 d['form'] = f
1137 e = ''
1138 for k,v in self.env.items():
1139 e += '\n %r=%r'%(k, v)
1140 d['env'] = e
1141 return '''
1142 form: %(form)s
1143 url: %(url)r
1144 base: %(base)r
1145 classname: %(classname)r
1146 template: %(template)r
1147 columns: %(columns)r
1148 sort: %(sort)r
1149 group: %(group)r
1150 filter: %(filter)r
1151 search_text: %(search_text)r
1152 pagesize: %(pagesize)r
1153 startwith: %(startwith)r
1154 env: %(env)s
1155 '''%d
1157 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1158 filterspec=1):
1159 ''' return the current index args as form elements '''
1160 l = []
1161 s = '<input type="hidden" name="%s" value="%s">'
1162 if columns and self.columns:
1163 l.append(s%(':columns', ','.join(self.columns)))
1164 if sort and self.sort[1] is not None:
1165 if self.sort[0] == '-':
1166 val = '-'+self.sort[1]
1167 else:
1168 val = self.sort[1]
1169 l.append(s%(':sort', val))
1170 if group and self.group[1] is not None:
1171 if self.group[0] == '-':
1172 val = '-'+self.group[1]
1173 else:
1174 val = self.group[1]
1175 l.append(s%(':group', val))
1176 if filter and self.filter:
1177 l.append(s%(':filter', ','.join(self.filter)))
1178 if filterspec:
1179 for k,v in self.filterspec.items():
1180 l.append(s%(k, ','.join(v)))
1181 if self.search_text:
1182 l.append(s%(':search_text', self.search_text))
1183 l.append(s%(':pagesize', self.pagesize))
1184 l.append(s%(':startwith', self.startwith))
1185 return '\n'.join(l)
1187 def indexargs_href(self, url, args):
1188 ''' embed the current index args in a URL '''
1189 l = ['%s=%s'%(k,v) for k,v in args.items()]
1190 if self.columns and not args.has_key(':columns'):
1191 l.append(':columns=%s'%(','.join(self.columns)))
1192 if self.sort[1] is not None and not args.has_key(':sort'):
1193 if self.sort[0] == '-':
1194 val = '-'+self.sort[1]
1195 else:
1196 val = self.sort[1]
1197 l.append(':sort=%s'%val)
1198 if self.group[1] is not None and not args.has_key(':group'):
1199 if self.group[0] == '-':
1200 val = '-'+self.group[1]
1201 else:
1202 val = self.group[1]
1203 l.append(':group=%s'%val)
1204 if self.filter and not args.has_key(':columns'):
1205 l.append(':filter=%s'%(','.join(self.filter)))
1206 for k,v in self.filterspec.items():
1207 if not args.has_key(k):
1208 l.append('%s=%s'%(k, ','.join(v)))
1209 if self.search_text and not args.has_key(':search_text'):
1210 l.append(':search_text=%s'%self.search_text)
1211 if not args.has_key(':pagesize'):
1212 l.append(':pagesize=%s'%self.pagesize)
1213 if not args.has_key(':startwith'):
1214 l.append(':startwith=%s'%self.startwith)
1215 return '%s?%s'%(url, '&'.join(l))
1217 def base_javascript(self):
1218 return '''
1219 <script language="javascript">
1220 submitted = false;
1221 function submit_once() {
1222 if (submitted) {
1223 alert("Your request is being processed.\\nPlease be patient.");
1224 return 0;
1225 }
1226 submitted = true;
1227 return 1;
1228 }
1230 function help_window(helpurl, width, height) {
1231 HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1232 }
1233 </script>
1234 '''%self.base
1236 def batch(self):
1237 ''' Return a batch object for results from the "current search"
1238 '''
1239 filterspec = self.filterspec
1240 sort = self.sort
1241 group = self.group
1243 # get the list of ids we're batching over
1244 klass = self.client.db.getclass(self.classname)
1245 if self.search_text:
1246 matches = self.client.db.indexer.search(
1247 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1248 else:
1249 matches = None
1250 l = klass.filter(matches, filterspec, sort, group)
1252 # return the batch object
1253 return Batch(self.client, self.classname, l, self.pagesize,
1254 self.startwith)
1257 # extend the standard ZTUtils Batch object to remove dependency on
1258 # Acquisition and add a couple of useful methods
1259 class Batch(ZTUtils.Batch):
1260 def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1261 self.client = client
1262 self.classname = classname
1263 self.last_index = self.last_item = None
1264 self.current_item = None
1265 ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1267 # overwrite so we can late-instantiate the HTMLItem instance
1268 def __getitem__(self, index):
1269 if index < 0:
1270 if index + self.end < self.first: raise IndexError, index
1271 return self._sequence[index + self.end]
1273 if index >= self.length: raise IndexError, index
1275 # move the last_item along - but only if the fetched index changes
1276 # (for some reason, index 0 is fetched twice)
1277 if index != self.last_index:
1278 self.last_item = self.current_item
1279 self.last_index = index
1281 # wrap the return in an HTMLItem
1282 if self.classname == 'user':
1283 klass = HTMLUser
1284 else:
1285 klass = HTMLItem
1286 self.current_item = klass(self.client, self.classname,
1287 self._sequence[index+self.first])
1288 return self.current_item
1290 def propchanged(self, property):
1291 ''' Detect if the property marked as being the group property
1292 changed in the last iteration fetch
1293 '''
1294 if (self.last_item is None or
1295 self.last_item[property] != self.current_item[property]):
1296 return 1
1297 return 0
1299 # override these 'cos we don't have access to acquisition
1300 def previous(self):
1301 if self.start == 1:
1302 return None
1303 return Batch(self.client, self.classname, self._sequence, self._size,
1304 self.first - self._size + self.overlap, 0, self.orphan,
1305 self.overlap)
1307 def next(self):
1308 try:
1309 self._sequence[self.end]
1310 except IndexError:
1311 return None
1312 return Batch(self.client, self.classname, self._sequence, self._size,
1313 self.end - self.overlap, 0, self.orphan, self.overlap)
1315 def length(self):
1316 self.sequence_length = l = len(self._sequence)
1317 return l