aea865120c74e74ecc7e398f55453e46c2ceac06
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 # Make sure these modules are loaded
20 # I need these to run PageTemplates outside of Zope :(
21 # If we're running in a Zope environment, these modules will be loaded
22 # already...
23 if not sys.modules.has_key('zLOG'):
24 import zLOG
25 sys.modules['zLOG'] = zLOG
26 if not sys.modules.has_key('MultiMapping'):
27 import MultiMapping
28 sys.modules['MultiMapping'] = MultiMapping
29 if not sys.modules.has_key('ComputedAttribute'):
30 import ComputedAttribute
31 sys.modules['ComputedAttribute'] = ComputedAttribute
32 if not sys.modules.has_key('ExtensionClass'):
33 import ExtensionClass
34 sys.modules['ExtensionClass'] = ExtensionClass
35 if not sys.modules.has_key('Acquisition'):
36 import Acquisition
37 sys.modules['Acquisition'] = Acquisition
39 # now it's safe to import PageTemplates, TAL and ZTUtils
40 from PageTemplates import PageTemplate
41 from PageTemplates.Expressions import getEngine
42 from TAL.TALInterpreter import TALInterpreter
43 import ZTUtils
45 # XXX WAH pagetemplates aren't pickleable :(
46 #def getTemplate(dir, name, classname=None, request=None):
47 # ''' Interface to get a template, possibly loading a compiled template.
48 # '''
49 # # source
50 # src = os.path.join(dir, name)
51 #
52 # # see if we can get a compile from the template"c" directory (most
53 # # likely is "htmlc"
54 # split = list(os.path.split(dir))
55 # split[-1] = split[-1] + 'c'
56 # cdir = os.path.join(*split)
57 # split.append(name)
58 # cpl = os.path.join(*split)
59 #
60 # # ok, now see if the source is newer than the compiled (or if the
61 # # compiled even exists)
62 # MTIME = os.path.stat.ST_MTIME
63 # if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
64 # # nope, we need to compile
65 # pt = RoundupPageTemplate()
66 # pt.write(open(src).read())
67 # pt.id = name
68 #
69 # # save off the compiled template
70 # if not os.path.exists(cdir):
71 # os.makedirs(cdir)
72 # f = open(cpl, 'wb')
73 # pickle.dump(pt, f)
74 # f.close()
75 # else:
76 # # yay, use the compiled template
77 # f = open(cpl, 'rb')
78 # pt = pickle.load(f)
79 # return pt
81 templates = {}
83 def getTemplate(dir, name, extension, classname=None, request=None):
84 ''' Interface to get a template, possibly loading a compiled template.
86 "name" and "extension" indicate the template we're after, which in
87 most cases will be "name.extension". If "extension" is None, then
88 we look for a template just called "name" with no extension.
90 If the file "name.extension" doesn't exist, we look for
91 "_generic.extension" as a fallback.
92 '''
93 # default the name to "home"
94 if name is None:
95 name = 'home'
97 # find the source, figure the time it was last modified
98 if extension:
99 filename = '%s.%s'%(name, extension)
100 else:
101 filename = name
102 src = os.path.join(dir, filename)
103 try:
104 stime = os.stat(src)[os.path.stat.ST_MTIME]
105 except os.error, error:
106 if error.errno != errno.ENOENT or not extension:
107 raise
108 # try for a generic template
109 filename = '_generic.%s'%extension
110 src = os.path.join(dir, filename)
111 stime = os.stat(src)[os.path.stat.ST_MTIME]
113 key = (dir, filename)
114 if templates.has_key(key) and stime < templates[key].mtime:
115 # compiled template is up to date
116 return templates[key]
118 # compile the template
119 templates[key] = pt = RoundupPageTemplate()
120 pt.write(open(src).read())
121 pt.id = name
122 pt.mtime = time.time()
123 return pt
125 class RoundupPageTemplate(PageTemplate.PageTemplate):
126 ''' A Roundup-specific PageTemplate.
128 Interrogate the client to set up the various template variables to
129 be available:
131 *class*
132 The current class of node being displayed as an HTMLClass
133 instance.
134 *item*
135 The current node from the database, if we're viewing a specific
136 node, as an HTMLItem instance. If it doesn't exist, then we're
137 on a new item page.
138 (*classname*)
139 this is one of two things:
141 1. the *item* is also available under its classname, so a *user*
142 node would also be available under the name *user*. This is
143 also an HTMLItem instance.
144 2. if there's no *item* then the current class is available
145 through this name, thus "user/name" and "user/name/menu" will
146 still work - the latter will pull information from the form
147 if it can.
148 *form*
149 The current CGI form information as a mapping of form argument
150 name to value
151 *request*
152 Includes information about the current request, including:
153 - the url
154 - the current index information (``filterspec``, ``filter`` args,
155 ``properties``, etc) parsed out of the form.
156 - methods for easy filterspec link generation
157 - *user*, the current user node as an HTMLItem instance
158 *instance*
159 The current instance
160 *db*
161 The current database, through which db.config may be reached.
163 Maybe also:
165 *modules*
166 python modules made available (XXX: not sure what's actually in
167 there tho)
168 '''
169 def getContext(self, client, classname, request):
170 c = {
171 'klass': HTMLClass(client, classname),
172 'options': {},
173 'nothing': None,
174 'request': request,
175 'content': client.content,
176 'db': HTMLDatabase(client),
177 'instance': client.instance
178 }
179 # add in the item if there is one
180 if client.nodeid:
181 c['item'] = HTMLItem(client.db, classname, client.nodeid)
182 c[classname] = c['item']
183 else:
184 c[classname] = c['klass']
185 return c
187 def render(self, client, classname, request, **options):
188 """Render this Page Template"""
190 if not self._v_cooked:
191 self._cook()
193 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
195 if self._v_errors:
196 raise PTRuntimeError, 'Page Template %s has errors.' % self.id
198 # figure the context
199 classname = classname or client.classname
200 request = request or HTMLRequest(client)
201 c = self.getContext(client, classname, request)
202 c.update({'options': options})
204 # and go
205 output = StringIO.StringIO()
206 TALInterpreter(self._v_program, self._v_macros,
207 getEngine().getContext(c), output, tal=1, strictinsert=0)()
208 return output.getvalue()
210 class HTMLDatabase:
211 ''' Return HTMLClasses for valid class fetches
212 '''
213 def __init__(self, client):
214 self.client = client
215 self.config = client.db.config
216 def __getattr__(self, attr):
217 try:
218 self.client.db.getclass(attr)
219 except KeyError:
220 raise AttributeError, attr
221 return HTMLClass(self.client, attr)
222 def classes(self):
223 l = self.client.db.classes.keys()
224 l.sort()
225 return [HTMLClass(self.client, cn) for cn in l]
227 class HTMLClass:
228 ''' Accesses through a class (either through *class* or *db.<classname>*)
229 '''
230 def __init__(self, client, classname):
231 self.client = client
232 self.db = client.db
233 self.classname = classname
234 if classname is not None:
235 self.klass = self.db.getclass(self.classname)
236 self.props = self.klass.getprops()
238 def __repr__(self):
239 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
241 def __getitem__(self, item):
242 ''' return an HTMLItem instance'''
243 #print 'getitem', (self, attr)
244 if item == 'creator':
245 return HTMLUser(self.client)
247 if not self.props.has_key(item):
248 raise KeyError, item
249 prop = self.props[item]
251 # look up the correct HTMLProperty class
252 for klass, htmlklass in propclasses:
253 if isinstance(prop, hyperdb.Multilink):
254 value = []
255 else:
256 value = None
257 if isinstance(prop, klass):
258 return htmlklass(self.db, '', prop, item, value)
260 # no good
261 raise KeyError, item
263 def __getattr__(self, attr):
264 ''' convenience access '''
265 try:
266 return self[attr]
267 except KeyError:
268 raise AttributeError, attr
270 def properties(self):
271 ''' Return HTMLProperty for all props
272 '''
273 l = []
274 for name, prop in self.props.items():
275 for klass, htmlklass in propclasses:
276 if isinstance(prop, hyperdb.Multilink):
277 value = []
278 else:
279 value = None
280 if isinstance(prop, klass):
281 l.append(htmlklass(self.db, '', prop, name, value))
282 return l
284 def list(self):
285 l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
286 return l
288 def csv(self):
289 ''' Return the items of this class as a chunk of CSV text.
290 '''
291 # get the CSV module
292 try:
293 import csv
294 except ImportError:
295 return 'Sorry, you need the csv module to use this function.\n'\
296 'Get it from: http://www.object-craft.com.au/projects/csv/'
298 props = self.propnames()
299 p = csv.parser()
300 s = StringIO.StringIO()
301 s.write(p.join(props) + '\n')
302 for nodeid in self.klass.list():
303 l = []
304 for name in props:
305 value = self.klass.get(nodeid, name)
306 if value is None:
307 l.append('')
308 elif isinstance(value, type([])):
309 l.append(':'.join(map(str, value)))
310 else:
311 l.append(str(self.klass.get(nodeid, name)))
312 s.write(p.join(l) + '\n')
313 return s.getvalue()
315 def propnames(self):
316 ''' Return the list of the names of the properties of this class.
317 '''
318 idlessprops = self.klass.getprops(protected=0).keys()
319 idlessprops.sort()
320 return ['id'] + idlessprops
322 def filter(self, request=None):
323 ''' Return a list of items from this class, filtered and sorted
324 by the current requested filterspec/filter/sort/group args
325 '''
326 if request is not None:
327 filterspec = request.filterspec
328 sort = request.sort
329 group = request.group
330 l = [HTMLItem(self.db, self.classname, x)
331 for x in self.klass.filter(None, filterspec, sort, group)]
332 return l
334 def classhelp(self, properties, label='?', width='400', height='400'):
335 '''pop up a javascript window with class help
337 This generates a link to a popup window which displays the
338 properties indicated by "properties" of the class named by
339 "classname". The "properties" should be a comma-separated list
340 (eg. 'id,name,description').
342 You may optionally override the label displayed, the width and
343 height. The popup window will be resizable and scrollable.
344 '''
345 return '<a href="javascript:help_window(\'%s?:template=help&' \
346 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(self.classname,
347 properties, width, height, label)
349 def submit(self, label="Submit New Entry"):
350 ''' Generate a submit button (and action hidden element)
351 '''
352 return ' <input type="hidden" name=":action" value="new">\n'\
353 ' <input type="submit" name="submit" value="%s">'%label
355 def history(self):
356 return 'New node - no history'
358 def renderWith(self, name, **kwargs):
359 ''' Render this class with the given template.
360 '''
361 # create a new request and override the specified args
362 req = HTMLRequest(self.client)
363 req.classname = self.classname
364 req.update(kwargs)
366 # new template, using the specified classname and request
367 pt = getTemplate(self.db.config.TEMPLATES, self.classname, name)
369 # XXX handle PT rendering errors here nicely
370 try:
371 # use our fabricated request
372 return pt.render(self.client, self.classname, req)
373 except PageTemplate.PTRuntimeError, message:
374 return '<strong>%s</strong><ol>%s</ol>'%(message,
375 cgi.escape('<li>'.join(pt._v_errors)))
377 class HTMLItem:
378 ''' Accesses through an *item*
379 '''
380 def __init__(self, db, classname, nodeid):
381 self.db = db
382 self.classname = classname
383 self.nodeid = nodeid
384 self.klass = self.db.getclass(classname)
385 self.props = self.klass.getprops()
387 def __repr__(self):
388 return '<HTMLItem(0x%x) %s %s>'%(id(self), self.classname, self.nodeid)
390 def __getitem__(self, item):
391 ''' return an HTMLItem instance'''
392 if item == 'id':
393 return self.nodeid
394 if not self.props.has_key(item):
395 raise KeyError, item
396 prop = self.props[item]
398 # get the value, handling missing values
399 value = self.klass.get(self.nodeid, item, None)
400 if value is None:
401 if isinstance(self.props[item], hyperdb.Multilink):
402 value = []
404 # look up the correct HTMLProperty class
405 for klass, htmlklass in propclasses:
406 if isinstance(prop, klass):
407 return htmlklass(self.db, self.nodeid, prop, item, value)
409 raise KeyErorr, item
411 def __getattr__(self, attr):
412 ''' convenience access to properties '''
413 try:
414 return self[attr]
415 except KeyError:
416 raise AttributeError, attr
418 def submit(self, label="Submit Changes"):
419 ''' Generate a submit button (and action hidden element)
420 '''
421 return ' <input type="hidden" name=":action" value="edit">\n'\
422 ' <input type="submit" name="submit" value="%s">'%label
424 # XXX this probably should just return the history items, not the HTML
425 def history(self, direction='descending'):
426 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
427 '<tr class="list-header">',
428 _('<th align=left><span class="list-item">Date</span></th>'),
429 _('<th align=left><span class="list-item">User</span></th>'),
430 _('<th align=left><span class="list-item">Action</span></th>'),
431 _('<th align=left><span class="list-item">Args</span></th>'),
432 '</tr>']
433 comments = {}
434 history = self.klass.history(self.nodeid)
435 history.sort()
436 if direction == 'descending':
437 history.reverse()
438 for id, evt_date, user, action, args in history:
439 date_s = str(evt_date).replace("."," ")
440 arg_s = ''
441 if action == 'link' 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 action == 'unlink' and type(args) == type(()):
450 if len(args) == 3:
451 linkcl, linkid, key = args
452 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
453 linkcl, linkid, key)
454 else:
455 arg_s = str(args)
457 elif type(args) == type({}):
458 cell = []
459 for k in args.keys():
460 # try to get the relevant property and treat it
461 # specially
462 try:
463 prop = self.props[k]
464 except KeyError:
465 prop = None
466 if prop is not None:
467 if args[k] and (isinstance(prop, hyperdb.Multilink) or
468 isinstance(prop, hyperdb.Link)):
469 # figure what the link class is
470 classname = prop.classname
471 try:
472 linkcl = self.db.getclass(classname)
473 except KeyError:
474 labelprop = None
475 comments[classname] = _('''The linked class
476 %(classname)s no longer exists''')%locals()
477 labelprop = linkcl.labelprop(1)
478 hrefable = os.path.exists(
479 os.path.join(self.db.config.TEMPLATES,
480 classname+'.item'))
482 if isinstance(prop, hyperdb.Multilink) and \
483 len(args[k]) > 0:
484 ml = []
485 for linkid in args[k]:
486 if isinstance(linkid, type(())):
487 sublabel = linkid[0] + ' '
488 linkids = linkid[1]
489 else:
490 sublabel = ''
491 linkids = [linkid]
492 subml = []
493 for linkid in linkids:
494 label = classname + linkid
495 # if we have a label property, try to use it
496 # TODO: test for node existence even when
497 # there's no labelprop!
498 try:
499 if labelprop is not None:
500 label = linkcl.get(linkid, labelprop)
501 except IndexError:
502 comments['no_link'] = _('''<strike>The
503 linked node no longer
504 exists</strike>''')
505 subml.append('<strike>%s</strike>'%label)
506 else:
507 if hrefable:
508 subml.append('<a href="%s%s">%s</a>'%(
509 classname, linkid, label))
510 ml.append(sublabel + ', '.join(subml))
511 cell.append('%s:\n %s'%(k, ', '.join(ml)))
512 elif isinstance(prop, hyperdb.Link) and args[k]:
513 label = classname + args[k]
514 # if we have a label property, try to use it
515 # TODO: test for node existence even when
516 # there's no labelprop!
517 if labelprop is not None:
518 try:
519 label = linkcl.get(args[k], labelprop)
520 except IndexError:
521 comments['no_link'] = _('''<strike>The
522 linked node no longer
523 exists</strike>''')
524 cell.append(' <strike>%s</strike>,\n'%label)
525 # "flag" this is done .... euwww
526 label = None
527 if label is not None:
528 if hrefable:
529 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
530 classname, args[k], label))
531 else:
532 cell.append('%s: %s' % (k,label))
534 elif isinstance(prop, hyperdb.Date) and args[k]:
535 d = date.Date(args[k])
536 cell.append('%s: %s'%(k, str(d)))
538 elif isinstance(prop, hyperdb.Interval) and args[k]:
539 d = date.Interval(args[k])
540 cell.append('%s: %s'%(k, str(d)))
542 elif isinstance(prop, hyperdb.String) and args[k]:
543 cell.append('%s: %s'%(k, cgi.escape(args[k])))
545 elif not args[k]:
546 cell.append('%s: (no value)\n'%k)
548 else:
549 cell.append('%s: %s\n'%(k, str(args[k])))
550 else:
551 # property no longer exists
552 comments['no_exist'] = _('''<em>The indicated property
553 no longer exists</em>''')
554 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
555 arg_s = '<br />'.join(cell)
556 else:
557 # unkown event!!
558 comments['unknown'] = _('''<strong><em>This event is not
559 handled by the history display!</em></strong>''')
560 arg_s = '<strong><em>' + str(args) + '</em></strong>'
561 date_s = date_s.replace(' ', ' ')
562 l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
563 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
564 user, action, arg_s))
565 if comments:
566 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
567 for entry in comments.values():
568 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
569 l.append('</table>')
570 return '\n'.join(l)
572 def remove(self):
573 # XXX do what?
574 return ''
576 class HTMLUser(HTMLItem):
577 ''' Accesses through the *user* (a special case of item)
578 '''
579 def __init__(self, client):
580 HTMLItem.__init__(self, client.db, 'user', client.userid)
581 self.default_classname = client.classname
582 self.userid = client.userid
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.userid, classname)
597 class HTMLProperty:
598 ''' String, Number, Date, Interval HTMLProperty
600 A wrapper object which may be stringified for the plain() behaviour.
601 '''
602 def __init__(self, db, nodeid, prop, name, value):
603 self.db = db
604 self.nodeid = nodeid
605 self.prop = prop
606 self.name = name
607 self.value = value
608 def __repr__(self):
609 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self.name, self.prop, self.value)
610 def __str__(self):
611 return self.plain()
612 def __cmp__(self, other):
613 if isinstance(other, HTMLProperty):
614 return cmp(self.value, other.value)
615 return cmp(self.value, other)
617 class StringHTMLProperty(HTMLProperty):
618 def plain(self, escape=0):
619 if self.value is None:
620 return ''
621 if escape:
622 return cgi.escape(str(self.value))
623 return str(self.value)
625 def stext(self, escape=0):
626 s = self.plain(escape=escape)
627 if not StructuredText:
628 return s
629 return StructuredText(s,level=1,header=0)
631 def field(self, size = 30):
632 if self.value is None:
633 value = ''
634 else:
635 value = cgi.escape(str(self.value))
636 value = '"'.join(value.split('"'))
637 return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
639 def multiline(self, escape=0, rows=5, cols=40):
640 if self.value is None:
641 value = ''
642 else:
643 value = cgi.escape(str(self.value))
644 value = '"'.join(value.split('"'))
645 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
646 self.name, rows, cols, value)
648 def email(self, escape=1):
649 ''' fudge email '''
650 if self.value is None: value = ''
651 else: value = str(self.value)
652 value = value.replace('@', ' at ')
653 value = value.replace('.', ' ')
654 if escape:
655 value = cgi.escape(value)
656 return value
658 class PasswordHTMLProperty(HTMLProperty):
659 def plain(self):
660 if self.value is None:
661 return ''
662 return _('*encrypted*')
664 def field(self, size = 30):
665 return '<input type="password" name="%s" size="%s">'%(self.name, size)
667 class NumberHTMLProperty(HTMLProperty):
668 def plain(self):
669 return str(self.value)
671 def field(self, size = 30):
672 if self.value is None:
673 value = ''
674 else:
675 value = cgi.escape(str(self.value))
676 value = '"'.join(value.split('"'))
677 return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
679 class BooleanHTMLProperty(HTMLProperty):
680 def plain(self):
681 if self.value is None:
682 return ''
683 return self.value and "Yes" or "No"
685 def field(self):
686 checked = self.value and "checked" or ""
687 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self.name,
688 checked)
689 if checked:
690 checked = ""
691 else:
692 checked = "checked"
693 s += '<input type="radio" name="%s" value="no" %s>No'%(self.name,
694 checked)
695 return s
697 class DateHTMLProperty(HTMLProperty):
698 def plain(self):
699 if self.value is None:
700 return ''
701 return str(self.value)
703 def field(self, size = 30):
704 if self.value is None:
705 value = ''
706 else:
707 value = cgi.escape(str(self.value))
708 value = '"'.join(value.split('"'))
709 return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
711 def reldate(self, pretty=1):
712 if not self.value:
713 return ''
715 # figure the interval
716 interval = date.Date('.') - self.value
717 if pretty:
718 return interval.pretty()
719 return str(interval)
721 class IntervalHTMLProperty(HTMLProperty):
722 def plain(self):
723 if self.value is None:
724 return ''
725 return str(self.value)
727 def pretty(self):
728 return self.value.pretty()
730 def field(self, size = 30):
731 if self.value is None:
732 value = ''
733 else:
734 value = cgi.escape(str(self.value))
735 value = '"'.join(value.split('"'))
736 return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
738 class LinkHTMLProperty(HTMLProperty):
739 ''' Link HTMLProperty
740 Include the above as well as being able to access the class
741 information. Stringifying the object itself results in the value
742 from the item being displayed. Accessing attributes of this object
743 result in the appropriate entry from the class being queried for the
744 property accessed (so item/assignedto/name would look up the user
745 entry identified by the assignedto property on item, and then the
746 name property of that user)
747 '''
748 def __getattr__(self, attr):
749 ''' return a new HTMLItem '''
750 #print 'getattr', (self, attr, self.value)
751 if not self.value:
752 raise AttributeError, "Can't access missing value"
753 i = HTMLItem(self.db, self.prop.classname, self.value)
754 return getattr(i, attr)
756 def plain(self, escape=0):
757 if self.value is None:
758 return _('[unselected]')
759 linkcl = self.db.classes[self.prop.classname]
760 k = linkcl.labelprop(1)
761 value = str(linkcl.get(self.value, k))
762 if escape:
763 value = cgi.escape(value)
764 return value
766 # XXX most of the stuff from here down is of dubious utility - it's easy
767 # enough to do in the template by hand (and in some cases, it's shorter
768 # and clearer...
770 def field(self):
771 linkcl = self.db.getclass(self.prop.classname)
772 if linkcl.getprops().has_key('order'):
773 sort_on = 'order'
774 else:
775 sort_on = linkcl.labelprop()
776 options = linkcl.filter(None, {}, [sort_on], [])
777 # TODO: make this a field display, not a menu one!
778 l = ['<select name="%s">'%property]
779 k = linkcl.labelprop(1)
780 if value is None:
781 s = 'selected '
782 else:
783 s = ''
784 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
785 for optionid in options:
786 option = linkcl.get(optionid, k)
787 s = ''
788 if optionid == value:
789 s = 'selected '
790 if showid:
791 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
792 else:
793 lab = option
794 if size is not None and len(lab) > size:
795 lab = lab[:size-3] + '...'
796 lab = cgi.escape(lab)
797 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
798 l.append('</select>')
799 return '\n'.join(l)
801 def download(self, showid=0):
802 linkname = self.prop.classname
803 linkcl = self.db.getclass(linkname)
804 k = linkcl.labelprop(1)
805 linkvalue = cgi.escape(str(linkcl.get(self.value, k)))
806 if showid:
807 label = value
808 title = ' title="%s"'%linkvalue
809 # note ... this should be urllib.quote(linkcl.get(value, k))
810 else:
811 label = linkvalue
812 title = ''
813 return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self.value,
814 linkvalue, title, label)
816 def menu(self, size=None, height=None, showid=0, additional=[],
817 **conditions):
818 value = self.value
820 # sort function
821 sortfunc = make_sort_function(self.db, self.prop.classname)
823 # force the value to be a single choice
824 if isinstance(value, type('')):
825 value = value[0]
826 linkcl = self.db.getclass(self.prop.classname)
827 l = ['<select name="%s">'%self.name]
828 k = linkcl.labelprop(1)
829 s = ''
830 if value is None:
831 s = 'selected '
832 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
833 if linkcl.getprops().has_key('order'):
834 sort_on = ('+', 'order')
835 else:
836 sort_on = ('+', linkcl.labelprop())
837 options = linkcl.filter(None, conditions, sort_on, (None, None))
838 for optionid in options:
839 option = linkcl.get(optionid, k)
840 s = ''
841 if value in [optionid, option]:
842 s = 'selected '
843 if showid:
844 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
845 else:
846 lab = option
847 if size is not None and len(lab) > size:
848 lab = lab[:size-3] + '...'
849 if additional:
850 m = []
851 for propname in additional:
852 m.append(linkcl.get(optionid, propname))
853 lab = lab + ' (%s)'%', '.join(map(str, m))
854 lab = cgi.escape(lab)
855 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
856 l.append('</select>')
857 return '\n'.join(l)
859 # def checklist(self, ...)
861 class MultilinkHTMLProperty(HTMLProperty):
862 ''' Multilink HTMLProperty
864 Also be iterable, returning a wrapper object like the Link case for
865 each entry in the multilink.
866 '''
867 def __len__(self):
868 ''' length of the multilink '''
869 return len(self.value)
871 def __getattr__(self, attr):
872 ''' no extended attribute accesses make sense here '''
873 raise AttributeError, attr
875 def __getitem__(self, num):
876 ''' iterate and return a new HTMLItem '''
877 #print 'getitem', (self, num)
878 value = self.value[num]
879 return HTMLItem(self.db, self.prop.classname, value)
881 def reverse(self):
882 ''' return the list in reverse order '''
883 l = self.value[:]
884 l.reverse()
885 return [HTMLItem(self.db, 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 # XXX most of the stuff from here down is of dubious utility - it's easy
899 # enough to do in the template by hand (and in some cases, it's shorter
900 # and clearer...
902 def field(self, size=30, showid=0):
903 sortfunc = make_sort_function(self.db, self.prop.classname)
904 linkcl = self.db.getclass(self.prop.classname)
905 value = self.value[:]
906 if value:
907 value.sort(sortfunc)
908 # map the id to the label property
909 if not showid:
910 k = linkcl.labelprop(1)
911 value = [linkcl.get(v, k) for v in value]
912 value = cgi.escape(','.join(value))
913 return '<input name="%s" size="%s" value="%s">'%(self.name, size, value)
915 def menu(self, size=None, height=None, showid=0, additional=[],
916 **conditions):
917 value = self.value
919 # sort function
920 sortfunc = make_sort_function(self.db, self.prop.classname)
922 linkcl = self.db.getclass(self.prop.classname)
923 if linkcl.getprops().has_key('order'):
924 sort_on = ('+', 'order')
925 else:
926 sort_on = ('+', linkcl.labelprop())
927 options = linkcl.filter(None, conditions, sort_on, (None,None))
928 height = height or min(len(options), 7)
929 l = ['<select multiple name="%s" size="%s">'%(self.name, height)]
930 k = linkcl.labelprop(1)
931 for optionid in options:
932 option = linkcl.get(optionid, k)
933 s = ''
934 if optionid in value or option in value:
935 s = 'selected '
936 if showid:
937 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
938 else:
939 lab = option
940 if size is not None and len(lab) > size:
941 lab = lab[:size-3] + '...'
942 if additional:
943 m = []
944 for propname in additional:
945 m.append(linkcl.get(optionid, propname))
946 lab = lab + ' (%s)'%', '.join(m)
947 lab = cgi.escape(lab)
948 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
949 lab))
950 l.append('</select>')
951 return '\n'.join(l)
953 # set the propclasses for HTMLItem
954 propclasses = (
955 (hyperdb.String, StringHTMLProperty),
956 (hyperdb.Number, NumberHTMLProperty),
957 (hyperdb.Boolean, BooleanHTMLProperty),
958 (hyperdb.Date, DateHTMLProperty),
959 (hyperdb.Interval, IntervalHTMLProperty),
960 (hyperdb.Password, PasswordHTMLProperty),
961 (hyperdb.Link, LinkHTMLProperty),
962 (hyperdb.Multilink, MultilinkHTMLProperty),
963 )
965 def make_sort_function(db, classname):
966 '''Make a sort function for a given class
967 '''
968 linkcl = db.getclass(classname)
969 if linkcl.getprops().has_key('order'):
970 sort_on = 'order'
971 else:
972 sort_on = linkcl.labelprop()
973 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
974 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
975 return sortfunc
977 def handleListCGIValue(value):
978 ''' Value is either a single item or a list of items. Each item has a
979 .value that we're actually interested in.
980 '''
981 if isinstance(value, type([])):
982 return [value.value for value in value]
983 else:
984 return value.value.split(',')
986 class ShowDict:
987 ''' A convenience access to the :columns index parameters
988 '''
989 def __init__(self, columns):
990 self.columns = {}
991 for col in columns:
992 self.columns[col] = 1
993 def __getitem__(self, name):
994 return self.columns.has_key(name)
996 class HTMLRequest:
997 ''' The *request*, holding the CGI form and environment.
999 "form" the CGI form as a cgi.FieldStorage
1000 "env" the CGI environment variables
1001 "url" the current URL path for this request
1002 "base" the base URL for this instance
1003 "user" a HTMLUser instance for this user
1004 "classname" the current classname (possibly None)
1005 "template" the current template (suffix, also possibly None)
1007 Index args:
1008 "columns" dictionary of the columns to display in an index page
1009 "show" a convenience access to columns - request/show/colname will
1010 be true if the columns should be displayed, false otherwise
1011 "sort" index sort column (direction, column name)
1012 "group" index grouping property (direction, column name)
1013 "filter" properties to filter the index on
1014 "filterspec" values to filter the index on
1015 "search_text" text to perform a full-text search on for an index
1017 '''
1018 def __init__(self, client):
1019 self.client = client
1021 # easier access vars
1022 self.form = client.form
1023 self.env = client.env
1024 self.base = client.base
1025 self.url = client.url
1026 self.user = HTMLUser(client)
1028 # store the current class name and action
1029 self.classname = client.classname
1030 self.template = client.template
1032 # extract the index display information from the form
1033 self.columns = []
1034 if self.form.has_key(':columns'):
1035 self.columns = handleListCGIValue(self.form[':columns'])
1036 self.show = ShowDict(self.columns)
1038 # sorting
1039 self.sort = (None, None)
1040 if self.form.has_key(':sort'):
1041 sort = self.form[':sort'].value
1042 if sort.startswith('-'):
1043 self.sort = ('-', sort[1:])
1044 else:
1045 self.sort = ('+', sort)
1046 if self.form.has_key(':sortdir'):
1047 self.sort = ('-', self.sort[1])
1049 # grouping
1050 self.group = (None, None)
1051 if self.form.has_key(':group'):
1052 group = self.form[':group'].value
1053 if group.startswith('-'):
1054 self.group = ('-', group[1:])
1055 else:
1056 self.group = ('+', group)
1057 if self.form.has_key(':groupdir'):
1058 self.group = ('-', self.group[1])
1060 # filtering
1061 self.filter = []
1062 if self.form.has_key(':filter'):
1063 self.filter = handleListCGIValue(self.form[':filter'])
1064 self.filterspec = {}
1065 if self.classname is not None:
1066 props = self.client.db.getclass(self.classname).getprops()
1067 for name in self.filter:
1068 if self.form.has_key(name):
1069 prop = props[name]
1070 fv = self.form[name]
1071 if (isinstance(prop, hyperdb.Link) or
1072 isinstance(prop, hyperdb.Multilink)):
1073 self.filterspec[name] = handleListCGIValue(fv)
1074 else:
1075 self.filterspec[name] = fv.value
1077 # full-text search argument
1078 self.search_text = None
1079 if self.form.has_key(':search_text'):
1080 self.search_text = self.form[':search_text'].value
1082 # pagination - size and start index
1083 # figure batch args
1084 if self.form.has_key(':pagesize'):
1085 self.pagesize = int(self.form[':pagesize'].value)
1086 else:
1087 self.pagesize = 50
1088 if self.form.has_key(':startwith'):
1089 self.startwith = int(self.form[':startwith'].value)
1090 else:
1091 self.startwith = 0
1093 def update(self, kwargs):
1094 self.__dict__.update(kwargs)
1095 if kwargs.has_key('columns'):
1096 self.show = ShowDict(self.columns)
1098 def __str__(self):
1099 d = {}
1100 d.update(self.__dict__)
1101 f = ''
1102 for k in self.form.keys():
1103 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1104 d['form'] = f
1105 e = ''
1106 for k,v in self.env.items():
1107 e += '\n %r=%r'%(k, v)
1108 d['env'] = e
1109 return '''
1110 form: %(form)s
1111 url: %(url)r
1112 base: %(base)r
1113 classname: %(classname)r
1114 template: %(template)r
1115 columns: %(columns)r
1116 sort: %(sort)r
1117 group: %(group)r
1118 filter: %(filter)r
1119 search_text: %(search_text)r
1120 pagesize: %(pagesize)r
1121 startwith: %(startwith)r
1122 env: %(env)s
1123 '''%d
1125 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1126 filterspec=1):
1127 ''' return the current index args as form elements '''
1128 l = []
1129 s = '<input type="hidden" name="%s" value="%s">'
1130 if columns and self.columns:
1131 l.append(s%(':columns', ','.join(self.columns)))
1132 if sort and self.sort[1] is not None:
1133 if self.sort[0] == '-':
1134 val = '-'+self.sort[1]
1135 else:
1136 val = self.sort[1]
1137 l.append(s%(':sort', val))
1138 if group and self.group[1] is not None:
1139 if self.group[0] == '-':
1140 val = '-'+self.group[1]
1141 else:
1142 val = self.group[1]
1143 l.append(s%(':group', val))
1144 if filter and self.filter:
1145 l.append(s%(':filter', ','.join(self.filter)))
1146 if filterspec:
1147 for k,v in self.filterspec.items():
1148 l.append(s%(k, ','.join(v)))
1149 if self.search_text:
1150 l.append(s%(':search_text', self.search_text))
1151 l.append(s%(':pagesize', self.pagesize))
1152 l.append(s%(':startwith', self.startwith))
1153 return '\n'.join(l)
1155 def indexargs_href(self, url, args):
1156 ''' embed the current index args in a URL '''
1157 l = ['%s=%s'%(k,v) for k,v in args.items()]
1158 if self.columns and not args.has_key(':columns'):
1159 l.append(':columns=%s'%(','.join(self.columns)))
1160 if self.sort[1] is not None and not args.has_key(':sort'):
1161 if self.sort[0] == '-':
1162 val = '-'+self.sort[1]
1163 else:
1164 val = self.sort[1]
1165 l.append(':sort=%s'%val)
1166 if self.group[1] is not None and not args.has_key(':group'):
1167 if self.group[0] == '-':
1168 val = '-'+self.group[1]
1169 else:
1170 val = self.group[1]
1171 l.append(':group=%s'%val)
1172 if self.filter and not args.has_key(':columns'):
1173 l.append(':filter=%s'%(','.join(self.filter)))
1174 for k,v in self.filterspec.items():
1175 if not args.has_key(k):
1176 l.append('%s=%s'%(k, ','.join(v)))
1177 if self.search_text and not args.has_key(':search_text'):
1178 l.append(':search_text=%s'%self.search_text)
1179 if not args.has_key(':pagesize'):
1180 l.append(':pagesize=%s'%self.pagesize)
1181 if not args.has_key(':startwith'):
1182 l.append(':startwith=%s'%self.startwith)
1183 return '%s?%s'%(url, '&'.join(l))
1185 def base_javascript(self):
1186 return '''
1187 <script language="javascript">
1188 submitted = false;
1189 function submit_once() {
1190 if (submitted) {
1191 alert("Your request is being processed.\\nPlease be patient.");
1192 return 0;
1193 }
1194 submitted = true;
1195 return 1;
1196 }
1198 function help_window(helpurl, width, height) {
1199 HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1200 }
1201 </script>
1202 '''%self.base
1204 def batch(self):
1205 ''' Return a batch object for results from the "current search"
1206 '''
1207 filterspec = self.filterspec
1208 sort = self.sort
1209 group = self.group
1211 # get the list of ids we're batching over
1212 klass = self.client.db.getclass(self.classname)
1213 if self.search_text:
1214 matches = self.client.db.indexer.search(
1215 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1216 else:
1217 matches = None
1218 l = klass.filter(matches, filterspec, sort, group)
1220 # return the batch object
1221 return Batch(self.client, self.classname, l, self.pagesize,
1222 self.startwith)
1225 # extend the standard ZTUtils Batch object to remove dependency on
1226 # Acquisition and add a couple of useful methods
1227 class Batch(ZTUtils.Batch):
1228 def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1229 self.client = client
1230 self.classname = classname
1231 self.last_index = self.last_item = None
1232 self.current_item = None
1233 ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1235 # overwrite so we can late-instantiate the HTMLItem instance
1236 def __getitem__(self, index):
1237 if index < 0:
1238 if index + self.end < self.first: raise IndexError, index
1239 return self._sequence[index + self.end]
1241 if index >= self.length: raise IndexError, index
1243 # move the last_item along - but only if the fetched index changes
1244 # (for some reason, index 0 is fetched twice)
1245 if index != self.last_index:
1246 self.last_item = self.current_item
1247 self.last_index = index
1249 # wrap the return in an HTMLItem
1250 self.current_item = HTMLItem(self.client.db, self.classname,
1251 self._sequence[index+self.first])
1252 return self.current_item
1254 def propchanged(self, property):
1255 ''' Detect if the property marked as being the group property
1256 changed in the last iteration fetch
1257 '''
1258 if (self.last_item is None or
1259 self.last_item[property] != self.current_item[property]):
1260 return 1
1261 return 0
1263 # override these 'cos we don't have access to acquisition
1264 def previous(self):
1265 if self.start == 1:
1266 return None
1267 return Batch(self.client, self.classname, self._sequence, self._size,
1268 self.first - self._size + self.overlap, 0, self.orphan,
1269 self.overlap)
1271 def next(self):
1272 try:
1273 self._sequence[self.end]
1274 except IndexError:
1275 return None
1276 return Batch(self.client, self.classname, self._sequence, self._size,
1277 self.end - self.overlap, 0, self.orphan, self.overlap)
1279 def length(self):
1280 self.sequence_length = l = len(self._sequence)
1281 return l