54a592b6341fa30cc21d1eeffc55e4c801edaa48
1 import sys, cgi, urllib, os, re
3 from roundup import hyperdb, date
4 from roundup.i18n import _
7 try:
8 import StructuredText
9 except ImportError:
10 StructuredText = None
12 # Make sure these modules are loaded
13 # I need these to run PageTemplates outside of Zope :(
14 # If we're running in a Zope environment, these modules will be loaded
15 # already...
16 if not sys.modules.has_key('zLOG'):
17 import zLOG
18 sys.modules['zLOG'] = zLOG
19 if not sys.modules.has_key('MultiMapping'):
20 import MultiMapping
21 sys.modules['MultiMapping'] = MultiMapping
22 if not sys.modules.has_key('ComputedAttribute'):
23 import ComputedAttribute
24 sys.modules['ComputedAttribute'] = ComputedAttribute
25 if not sys.modules.has_key('ExtensionClass'):
26 import ExtensionClass
27 sys.modules['ExtensionClass'] = ExtensionClass
28 if not sys.modules.has_key('Acquisition'):
29 import Acquisition
30 sys.modules['Acquisition'] = Acquisition
32 # now it's safe to import PageTemplates and ZTUtils
33 from PageTemplates import PageTemplate
34 import ZTUtils
36 class RoundupPageTemplate(PageTemplate.PageTemplate):
37 ''' A Roundup-specific PageTemplate.
39 Interrogate the client to set up the various template variables to
40 be available:
42 *class*
43 The current class of node being displayed as an HTMLClass
44 instance.
45 *item*
46 The current node from the database, if we're viewing a specific
47 node, as an HTMLItem instance. If it doesn't exist, then we're
48 on a new item page.
49 (*classname*)
50 this is one of two things:
52 1. the *item* is also available under its classname, so a *user*
53 node would also be available under the name *user*. This is
54 also an HTMLItem instance.
55 2. if there's no *item* then the current class is available
56 through this name, thus "user/name" and "user/name/menu" will
57 still work - the latter will pull information from the form
58 if it can.
59 *form*
60 The current CGI form information as a mapping of form argument
61 name to value
62 *request*
63 Includes information about the current request, including:
64 - the url
65 - the current index information (``filterspec``, ``filter`` args,
66 ``properties``, etc) parsed out of the form.
67 - methods for easy filterspec link generation
68 - *user*, the current user node as an HTMLItem instance
69 *instance*
70 The current instance
71 *db*
72 The current database, through which db.config may be reached.
74 Maybe also:
76 *modules*
77 python modules made available (XXX: not sure what's actually in
78 there tho)
79 '''
80 def __init__(self, client, classname=None, request=None):
81 ''' Extract the vars from the client and install in the context.
82 '''
83 self.client = client
84 self.classname = classname or self.client.classname
85 self.request = request or HTMLRequest(self.client)
87 def pt_getContext(self):
88 c = {
89 'klass': HTMLClass(self.client, self.classname),
90 'options': {},
91 'nothing': None,
92 'request': self.request,
93 'content': self.client.content,
94 'db': HTMLDatabase(self.client),
95 'instance': self.client.instance
96 }
97 # add in the item if there is one
98 if self.client.nodeid:
99 c['item'] = HTMLItem(self.client.db, self.classname,
100 self.client.nodeid)
101 c[self.classname] = c['item']
102 else:
103 c[self.classname] = c['klass']
104 return c
106 def render(self, *args, **kwargs):
107 if not kwargs.has_key('args'):
108 kwargs['args'] = args
109 return self.pt_render(extra_context={'options': kwargs})
111 class HTMLDatabase:
112 ''' Return HTMLClasses for valid class fetches
113 '''
114 def __init__(self, client):
115 self.client = client
116 self.config = client.db.config
117 def __getattr__(self, attr):
118 self.client.db.getclass(attr)
119 return HTMLClass(self.client, attr)
120 def classes(self):
121 l = self.client.db.classes.keys()
122 l.sort()
123 return [HTMLClass(self.client, cn) for cn in l]
125 class HTMLClass:
126 ''' Accesses through a class (either through *class* or *db.<classname>*)
127 '''
128 def __init__(self, client, classname):
129 self.client = client
130 self.db = client.db
131 self.classname = classname
132 if classname is not None:
133 self.klass = self.db.getclass(self.classname)
134 self.props = self.klass.getprops()
136 def __repr__(self):
137 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
139 def __getitem__(self, item):
140 ''' return an HTMLItem instance'''
141 #print 'getitem', (self, attr)
142 if item == 'creator':
143 return HTMLUser(self.client)
145 if not self.props.has_key(item):
146 raise KeyError, item
147 prop = self.props[item]
149 # look up the correct HTMLProperty class
150 for klass, htmlklass in propclasses:
151 if isinstance(prop, hyperdb.Multilink):
152 value = []
153 else:
154 value = None
155 if isinstance(prop, klass):
156 return htmlklass(self.db, '', prop, item, value)
158 # no good
159 raise KeyError, item
161 def __getattr__(self, attr):
162 ''' convenience access '''
163 try:
164 return self[attr]
165 except KeyError:
166 raise AttributeError, attr
168 def properties(self):
169 ''' Return HTMLProperty for all props
170 '''
171 l = []
172 for name, prop in self.props.items():
173 for klass, htmlklass in propclasses:
174 if isinstance(prop, hyperdb.Multilink):
175 value = []
176 else:
177 value = None
178 if isinstance(prop, klass):
179 l.append(htmlklass(self.db, '', prop, name, value))
180 return l
182 def list(self):
183 l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
184 return l
186 def filter(self, request=None):
187 ''' Return a list of items from this class, filtered and sorted
188 by the current requested filterspec/filter/sort/group args
189 '''
190 if request is not None:
191 filterspec = request.filterspec
192 sort = request.sort
193 group = request.group
194 l = [HTMLItem(self.db, self.classname, x)
195 for x in self.klass.filter(None, filterspec, sort, group)]
196 return l
198 def classhelp(self, properties, label='?', width='400', height='400'):
199 '''pop up a javascript window with class help
201 This generates a link to a popup window which displays the
202 properties indicated by "properties" of the class named by
203 "classname". The "properties" should be a comma-separated list
204 (eg. 'id,name,description').
206 You may optionally override the label displayed, the width and
207 height. The popup window will be resizable and scrollable.
208 '''
209 return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
210 'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(self.classname,
211 properties, width, height, label)
213 def submit(self, label="Submit New Entry"):
214 ''' Generate a submit button (and action hidden element)
215 '''
216 return ' <input type="hidden" name=":action" value="new">\n'\
217 ' <input type="submit" name="submit" value="%s">'%label
219 def history(self):
220 return 'New node - no history'
222 def renderWith(self, name, **kwargs):
223 ''' Render this class with the given template.
224 '''
225 # create a new request and override the specified args
226 req = HTMLRequest(self.client)
227 req.classname = self.classname
228 req.__dict__.update(kwargs)
230 # new template, using the specified classname and request
231 pt = RoundupPageTemplate(self.client, self.classname, req)
233 # use the specified template
234 name = self.classname + '.' + name
235 pt.write(open('/tmp/test/html/%s'%name).read())
236 pt.id = name
238 # XXX handle PT rendering errors here nicely
239 try:
240 return pt.render()
241 except PageTemplate.PTRuntimeError, message:
242 return '<strong>%s</strong><ol>%s</ol>'%(message,
243 cgi.escape('<li>'.join(pt._v_errors)))
245 class HTMLItem:
246 ''' Accesses through an *item*
247 '''
248 def __init__(self, db, classname, nodeid):
249 self.db = db
250 self.classname = classname
251 self.nodeid = nodeid
252 self.klass = self.db.getclass(classname)
253 self.props = self.klass.getprops()
255 def __repr__(self):
256 return '<HTMLItem(0x%x) %s %s>'%(id(self), self.classname, self.nodeid)
258 def __getitem__(self, item):
259 ''' return an HTMLItem instance'''
260 if item == 'id':
261 return self.nodeid
262 if not self.props.has_key(item):
263 raise KeyError, item
264 prop = self.props[item]
266 # get the value, handling missing values
267 value = self.klass.get(self.nodeid, item, None)
268 if value is None:
269 if isinstance(self.props[item], hyperdb.Multilink):
270 value = []
272 # look up the correct HTMLProperty class
273 for klass, htmlklass in propclasses:
274 if isinstance(prop, klass):
275 return htmlklass(self.db, self.nodeid, prop, item, value)
277 raise KeyErorr, item
279 def __getattr__(self, attr):
280 ''' convenience access to properties '''
281 try:
282 return self[attr]
283 except KeyError:
284 raise AttributeError, attr
286 def submit(self, label="Submit Changes"):
287 ''' Generate a submit button (and action hidden element)
288 '''
289 return ' <input type="hidden" name=":action" value="edit">\n'\
290 ' <input type="submit" name="submit" value="%s">'%label
292 # XXX this probably should just return the history items, not the HTML
293 def history(self, direction='descending'):
294 l = ['<table width=100% border=0 cellspacing=0 cellpadding=2>',
295 '<tr class="list-header">',
296 _('<th align=left><span class="list-item">Date</span></th>'),
297 _('<th align=left><span class="list-item">User</span></th>'),
298 _('<th align=left><span class="list-item">Action</span></th>'),
299 _('<th align=left><span class="list-item">Args</span></th>'),
300 '</tr>']
301 comments = {}
302 history = self.klass.history(self.nodeid)
303 history.sort()
304 if direction == 'descending':
305 history.reverse()
306 for id, evt_date, user, action, args in history:
307 date_s = str(evt_date).replace("."," ")
308 arg_s = ''
309 if action == 'link' and type(args) == type(()):
310 if len(args) == 3:
311 linkcl, linkid, key = args
312 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
313 linkcl, linkid, key)
314 else:
315 arg_s = str(args)
317 elif action == 'unlink' and type(args) == type(()):
318 if len(args) == 3:
319 linkcl, linkid, key = args
320 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
321 linkcl, linkid, key)
322 else:
323 arg_s = str(args)
325 elif type(args) == type({}):
326 cell = []
327 for k in args.keys():
328 # try to get the relevant property and treat it
329 # specially
330 try:
331 prop = self.props[k]
332 except KeyError:
333 prop = None
334 if prop is not None:
335 if args[k] and (isinstance(prop, hyperdb.Multilink) or
336 isinstance(prop, hyperdb.Link)):
337 # figure what the link class is
338 classname = prop.classname
339 try:
340 linkcl = self.db.getclass(classname)
341 except KeyError:
342 labelprop = None
343 comments[classname] = _('''The linked class
344 %(classname)s no longer exists''')%locals()
345 labelprop = linkcl.labelprop(1)
346 hrefable = os.path.exists(
347 os.path.join(self.db.config.TEMPLATES,
348 classname+'.item'))
350 if isinstance(prop, hyperdb.Multilink) and \
351 len(args[k]) > 0:
352 ml = []
353 for linkid in args[k]:
354 if isinstance(linkid, type(())):
355 sublabel = linkid[0] + ' '
356 linkids = linkid[1]
357 else:
358 sublabel = ''
359 linkids = [linkid]
360 subml = []
361 for linkid in linkids:
362 label = classname + linkid
363 # if we have a label property, try to use it
364 # TODO: test for node existence even when
365 # there's no labelprop!
366 try:
367 if labelprop is not None:
368 label = linkcl.get(linkid, labelprop)
369 except IndexError:
370 comments['no_link'] = _('''<strike>The
371 linked node no longer
372 exists</strike>''')
373 subml.append('<strike>%s</strike>'%label)
374 else:
375 if hrefable:
376 subml.append('<a href="%s%s">%s</a>'%(
377 classname, linkid, label))
378 ml.append(sublabel + ', '.join(subml))
379 cell.append('%s:\n %s'%(k, ', '.join(ml)))
380 elif isinstance(prop, hyperdb.Link) and args[k]:
381 label = classname + args[k]
382 # if we have a label property, try to use it
383 # TODO: test for node existence even when
384 # there's no labelprop!
385 if labelprop is not None:
386 try:
387 label = linkcl.get(args[k], labelprop)
388 except IndexError:
389 comments['no_link'] = _('''<strike>The
390 linked node no longer
391 exists</strike>''')
392 cell.append(' <strike>%s</strike>,\n'%label)
393 # "flag" this is done .... euwww
394 label = None
395 if label is not None:
396 if hrefable:
397 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
398 classname, args[k], label))
399 else:
400 cell.append('%s: %s' % (k,label))
402 elif isinstance(prop, hyperdb.Date) and args[k]:
403 d = date.Date(args[k])
404 cell.append('%s: %s'%(k, str(d)))
406 elif isinstance(prop, hyperdb.Interval) and args[k]:
407 d = date.Interval(args[k])
408 cell.append('%s: %s'%(k, str(d)))
410 elif isinstance(prop, hyperdb.String) and args[k]:
411 cell.append('%s: %s'%(k, cgi.escape(args[k])))
413 elif not args[k]:
414 cell.append('%s: (no value)\n'%k)
416 else:
417 cell.append('%s: %s\n'%(k, str(args[k])))
418 else:
419 # property no longer exists
420 comments['no_exist'] = _('''<em>The indicated property
421 no longer exists</em>''')
422 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
423 arg_s = '<br />'.join(cell)
424 else:
425 # unkown event!!
426 comments['unknown'] = _('''<strong><em>This event is not
427 handled by the history display!</em></strong>''')
428 arg_s = '<strong><em>' + str(args) + '</em></strong>'
429 date_s = date_s.replace(' ', ' ')
430 l.append('<tr><td nowrap valign=top>%s</td><td valign=top>%s</td>'
431 '<td valign=top>%s</td><td valign=top>%s</td></tr>'%(date_s,
432 user, action, arg_s))
433 if comments:
434 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
435 for entry in comments.values():
436 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
437 l.append('</table>')
438 return '\n'.join(l)
440 def remove(self):
441 # XXX do what?
442 return ''
444 class HTMLUser(HTMLItem):
445 ''' Accesses through the *user* (a special case of item)
446 '''
447 def __init__(self, client):
448 HTMLItem.__init__(self, client.db, 'user', client.userid)
449 self.default_classname = client.classname
450 self.userid = client.userid
452 # used for security checks
453 self.security = client.db.security
454 _marker = []
455 def hasPermission(self, role, classname=_marker):
456 ''' Determine if the user has the Role.
458 The class being tested defaults to the template's class, but may
459 be overidden for this test by suppling an alternate classname.
460 '''
461 if classname is self._marker:
462 classname = self.default_classname
463 return self.security.hasPermission(role, self.userid, classname)
465 class HTMLProperty:
466 ''' String, Number, Date, Interval HTMLProperty
468 A wrapper object which may be stringified for the plain() behaviour.
469 '''
470 def __init__(self, db, nodeid, prop, name, value):
471 self.db = db
472 self.nodeid = nodeid
473 self.prop = prop
474 self.name = name
475 self.value = value
476 def __repr__(self):
477 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self.name, self.prop, self.value)
478 def __str__(self):
479 return self.plain()
480 def __cmp__(self, other):
481 if isinstance(other, HTMLProperty):
482 return cmp(self.value, other.value)
483 return cmp(self.value, other)
485 class StringHTMLProperty(HTMLProperty):
486 def plain(self, escape=0):
487 if self.value is None:
488 return ''
489 if escape:
490 return cgi.escape(str(self.value))
491 return str(self.value)
493 def stext(self, escape=0):
494 s = self.plain(escape=escape)
495 if not StructuredText:
496 return s
497 return StructuredText(s,level=1,header=0)
499 def field(self, size = 30):
500 if self.value is None:
501 value = ''
502 else:
503 value = cgi.escape(str(self.value))
504 value = '"'.join(value.split('"'))
505 return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
507 def multiline(self, escape=0, rows=5, cols=40):
508 if self.value is None:
509 value = ''
510 else:
511 value = cgi.escape(str(self.value))
512 value = '"'.join(value.split('"'))
513 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
514 self.name, rows, cols, value)
516 def email(self, escape=1):
517 ''' fudge email '''
518 if self.value is None: value = ''
519 else: value = str(self.value)
520 value = value.replace('@', ' at ')
521 value = value.replace('.', ' ')
522 if escape:
523 value = cgi.escape(value)
524 return value
526 class PasswordHTMLProperty(HTMLProperty):
527 def plain(self):
528 if self.value is None:
529 return ''
530 return _('*encrypted*')
532 def field(self, size = 30):
533 return '<input type="password" name="%s" size="%s">'%(self.name, size)
535 class NumberHTMLProperty(HTMLProperty):
536 def plain(self):
537 return str(self.value)
539 def field(self, size = 30):
540 if self.value is None:
541 value = ''
542 else:
543 value = cgi.escape(str(self.value))
544 value = '"'.join(value.split('"'))
545 return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
547 class BooleanHTMLProperty(HTMLProperty):
548 def plain(self):
549 if self.value is None:
550 return ''
551 return self.value and "Yes" or "No"
553 def field(self):
554 checked = self.value and "checked" or ""
555 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self.name,
556 checked)
557 if checked:
558 checked = ""
559 else:
560 checked = "checked"
561 s += '<input type="radio" name="%s" value="no" %s>No'%(self.name,
562 checked)
563 return s
565 class DateHTMLProperty(HTMLProperty):
566 def plain(self):
567 if self.value is None:
568 return ''
569 return str(self.value)
571 def field(self, size = 30):
572 if self.value is None:
573 value = ''
574 else:
575 value = cgi.escape(str(self.value))
576 value = '"'.join(value.split('"'))
577 return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
579 def reldate(self, pretty=1):
580 if not self.value:
581 return ''
583 # figure the interval
584 interval = date.Date('.') - self.value
585 if pretty:
586 return interval.pretty()
587 return str(interval)
589 class IntervalHTMLProperty(HTMLProperty):
590 def plain(self):
591 if self.value is None:
592 return ''
593 return str(self.value)
595 def pretty(self):
596 return self.value.pretty()
598 def field(self, size = 30):
599 if self.value is None:
600 value = ''
601 else:
602 value = cgi.escape(str(self.value))
603 value = '"'.join(value.split('"'))
604 return '<input name="%s" value="%s" size="%s">'%(self.name, value, size)
606 class LinkHTMLProperty(HTMLProperty):
607 ''' Link HTMLProperty
608 Include the above as well as being able to access the class
609 information. Stringifying the object itself results in the value
610 from the item being displayed. Accessing attributes of this object
611 result in the appropriate entry from the class being queried for the
612 property accessed (so item/assignedto/name would look up the user
613 entry identified by the assignedto property on item, and then the
614 name property of that user)
615 '''
616 def __getattr__(self, attr):
617 ''' return a new HTMLItem '''
618 #print 'getattr', (self, attr, self.value)
619 if not self.value:
620 raise AttributeError, "Can't access missing value"
621 i = HTMLItem(self.db, self.prop.classname, self.value)
622 return getattr(i, attr)
624 def plain(self, escape=0):
625 if self.value is None:
626 return _('[unselected]')
627 linkcl = self.db.classes[self.prop.classname]
628 k = linkcl.labelprop(1)
629 value = str(linkcl.get(self.value, k))
630 if escape:
631 value = cgi.escape(value)
632 return value
634 # XXX most of the stuff from here down is of dubious utility - it's easy
635 # enough to do in the template by hand (and in some cases, it's shorter
636 # and clearer...
638 def field(self):
639 linkcl = self.db.getclass(self.prop.classname)
640 if linkcl.getprops().has_key('order'):
641 sort_on = 'order'
642 else:
643 sort_on = linkcl.labelprop()
644 options = linkcl.filter(None, {}, [sort_on], [])
645 # TODO: make this a field display, not a menu one!
646 l = ['<select name="%s">'%property]
647 k = linkcl.labelprop(1)
648 if value is None:
649 s = 'selected '
650 else:
651 s = ''
652 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
653 for optionid in options:
654 option = linkcl.get(optionid, k)
655 s = ''
656 if optionid == value:
657 s = 'selected '
658 if showid:
659 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
660 else:
661 lab = option
662 if size is not None and len(lab) > size:
663 lab = lab[:size-3] + '...'
664 lab = cgi.escape(lab)
665 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
666 l.append('</select>')
667 return '\n'.join(l)
669 def download(self, showid=0):
670 linkname = self.prop.classname
671 linkcl = self.db.getclass(linkname)
672 k = linkcl.labelprop(1)
673 linkvalue = cgi.escape(str(linkcl.get(self.value, k)))
674 if showid:
675 label = value
676 title = ' title="%s"'%linkvalue
677 # note ... this should be urllib.quote(linkcl.get(value, k))
678 else:
679 label = linkvalue
680 title = ''
681 return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self.value,
682 linkvalue, title, label)
684 def menu(self, size=None, height=None, showid=0, additional=[],
685 **conditions):
686 value = self.value
688 # sort function
689 sortfunc = make_sort_function(self.db, self.prop.classname)
691 # force the value to be a single choice
692 if isinstance(value, type('')):
693 value = value[0]
694 linkcl = self.db.getclass(self.prop.classname)
695 l = ['<select name="%s">'%self.name]
696 k = linkcl.labelprop(1)
697 s = ''
698 if value is None:
699 s = 'selected '
700 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
701 if linkcl.getprops().has_key('order'):
702 sort_on = ('+', 'order')
703 else:
704 sort_on = ('+', linkcl.labelprop())
705 options = linkcl.filter(None, conditions, sort_on, (None, None))
706 for optionid in options:
707 option = linkcl.get(optionid, k)
708 s = ''
709 if value in [optionid, option]:
710 s = 'selected '
711 if showid:
712 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
713 else:
714 lab = option
715 if size is not None and len(lab) > size:
716 lab = lab[:size-3] + '...'
717 if additional:
718 m = []
719 for propname in additional:
720 m.append(linkcl.get(optionid, propname))
721 lab = lab + ' (%s)'%', '.join(map(str, m))
722 lab = cgi.escape(lab)
723 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
724 l.append('</select>')
725 return '\n'.join(l)
727 # def checklist(self, ...)
729 class MultilinkHTMLProperty(HTMLProperty):
730 ''' Multilink HTMLProperty
732 Also be iterable, returning a wrapper object like the Link case for
733 each entry in the multilink.
734 '''
735 def __len__(self):
736 ''' length of the multilink '''
737 return len(self.value)
739 def __getattr__(self, attr):
740 ''' no extended attribute accesses make sense here '''
741 raise AttributeError, attr
743 def __getitem__(self, num):
744 ''' iterate and return a new HTMLItem '''
745 #print 'getitem', (self, num)
746 value = self.value[num]
747 return HTMLItem(self.db, self.prop.classname, value)
749 def reverse(self):
750 ''' return the list in reverse order '''
751 l = self.value[:]
752 l.reverse()
753 return [HTMLItem(self.db, self.prop.classname, value) for value in l]
755 def plain(self, escape=0):
756 linkcl = self.db.classes[self.prop.classname]
757 k = linkcl.labelprop(1)
758 labels = []
759 for v in self.value:
760 labels.append(linkcl.get(v, k))
761 value = ', '.join(labels)
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, size=30, showid=0):
771 sortfunc = make_sort_function(self.db, self.prop.classname)
772 linkcl = self.db.getclass(self.prop.classname)
773 value = self.value[:]
774 if value:
775 value.sort(sortfunc)
776 # map the id to the label property
777 if not showid:
778 k = linkcl.labelprop(1)
779 value = [linkcl.get(v, k) for v in value]
780 value = cgi.escape(','.join(value))
781 return '<input name="%s" size="%s" value="%s">'%(self.name, size, value)
783 def menu(self, size=None, height=None, showid=0, additional=[],
784 **conditions):
785 value = self.value
787 # sort function
788 sortfunc = make_sort_function(self.db, self.prop.classname)
790 linkcl = self.db.getclass(self.prop.classname)
791 if linkcl.getprops().has_key('order'):
792 sort_on = ('+', 'order')
793 else:
794 sort_on = ('+', linkcl.labelprop())
795 options = linkcl.filter(None, conditions, sort_on, (None,None))
796 height = height or min(len(options), 7)
797 l = ['<select multiple name="%s" size="%s">'%(self.name, height)]
798 k = linkcl.labelprop(1)
799 for optionid in options:
800 option = linkcl.get(optionid, k)
801 s = ''
802 if optionid in value or option in value:
803 s = 'selected '
804 if showid:
805 lab = '%s%s: %s'%(self.prop.classname, optionid, option)
806 else:
807 lab = option
808 if size is not None and len(lab) > size:
809 lab = lab[:size-3] + '...'
810 if additional:
811 m = []
812 for propname in additional:
813 m.append(linkcl.get(optionid, propname))
814 lab = lab + ' (%s)'%', '.join(m)
815 lab = cgi.escape(lab)
816 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
817 lab))
818 l.append('</select>')
819 return '\n'.join(l)
821 # set the propclasses for HTMLItem
822 propclasses = (
823 (hyperdb.String, StringHTMLProperty),
824 (hyperdb.Number, NumberHTMLProperty),
825 (hyperdb.Boolean, BooleanHTMLProperty),
826 (hyperdb.Date, DateHTMLProperty),
827 (hyperdb.Interval, IntervalHTMLProperty),
828 (hyperdb.Password, PasswordHTMLProperty),
829 (hyperdb.Link, LinkHTMLProperty),
830 (hyperdb.Multilink, MultilinkHTMLProperty),
831 )
833 def make_sort_function(db, classname):
834 '''Make a sort function for a given class
835 '''
836 linkcl = db.getclass(classname)
837 if linkcl.getprops().has_key('order'):
838 sort_on = 'order'
839 else:
840 sort_on = linkcl.labelprop()
841 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
842 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
843 return sortfunc
845 def handleListCGIValue(value):
846 ''' Value is either a single item or a list of items. Each item has a
847 .value that we're actually interested in.
848 '''
849 if isinstance(value, type([])):
850 return [value.value for value in value]
851 else:
852 return value.value.split(',')
854 class HTMLRequest:
855 ''' The *request*, holding the CGI form and environment.
857 "form" the CGI form as a cgi.FieldStorage
858 "env" the CGI environment variables
859 "url" the current URL path for this request
860 "base" the base URL for this instance
861 "user" a HTMLUser instance for this user
862 "classname" the current classname (possibly None)
863 "template_type" the current template type (suffix, also possibly None)
865 Index args:
866 "columns" dictionary of the columns to display in an index page
867 "sort" index sort column (direction, column name)
868 "group" index grouping property (direction, column name)
869 "filter" properties to filter the index on
870 "filterspec" values to filter the index on
871 "search_text" text to perform a full-text search on for an index
873 '''
874 def __init__(self, client):
875 self.client = client
877 # easier access vars
878 self.form = client.form
879 self.env = client.env
880 self.base = client.base
881 self.url = client.url
882 self.user = HTMLUser(client)
884 # store the current class name and action
885 self.classname = client.classname
886 self.template_type = client.template_type
888 # extract the index display information from the form
889 self.columns = {}
890 if self.form.has_key(':columns'):
891 for entry in handleListCGIValue(self.form[':columns']):
892 self.columns[entry] = 1
894 # sorting
895 self.sort = (None, None)
896 if self.form.has_key(':sort'):
897 sort = self.form[':sort'].value
898 if sort.startswith('-'):
899 self.sort = ('-', sort[1:])
900 else:
901 self.sort = ('+', sort)
902 if self.form.has_key(':sortdir'):
903 self.sort = ('-', self.sort[1])
905 # grouping
906 self.group = (None, None)
907 if self.form.has_key(':group'):
908 group = self.form[':group'].value
909 if group.startswith('-'):
910 self.group = ('-', group[1:])
911 else:
912 self.group = ('+', group)
913 if self.form.has_key(':groupdir'):
914 self.group = ('-', self.group[1])
916 # filtering
917 self.filter = []
918 if self.form.has_key(':filter'):
919 self.filter = handleListCGIValue(self.form[':filter'])
920 self.filterspec = {}
921 if self.classname is not None:
922 props = self.client.db.getclass(self.classname).getprops()
923 for name in self.filter:
924 if self.form.has_key(name):
925 prop = props[name]
926 fv = self.form[name]
927 if (isinstance(prop, hyperdb.Link) or
928 isinstance(prop, hyperdb.Multilink)):
929 self.filterspec[name] = handleListCGIValue(fv)
930 else:
931 self.filterspec[name] = fv.value
933 # full-text search argument
934 self.search_text = None
935 if self.form.has_key(':search_text'):
936 self.search_text = self.form[':search_text'].value
938 def __str__(self):
939 d = {}
940 d.update(self.__dict__)
941 f = ''
942 for k in self.form.keys():
943 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
944 d['form'] = f
945 e = ''
946 for k,v in self.env.items():
947 e += '\n %r=%r'%(k, v)
948 d['env'] = e
949 return '''
950 form: %(form)s
951 url: %(url)r
952 base: %(base)r
953 classname: %(classname)r
954 template_type: %(template_type)r
955 columns: %(columns)r
956 sort: %(sort)r
957 group: %(group)r
958 filter: %(filter)r
959 filterspec: %(filterspec)r
960 env: %(env)s
961 '''%d
963 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
964 filterspec=1):
965 ''' return the current index args as form elements '''
966 l = []
967 s = '<input type="hidden" name="%s" value="%s">'
968 if columns and self.columns:
969 l.append(s%(':columns', ','.join(self.columns.keys())))
970 if sort and self.sort is not None:
971 if self.sort[0] == '-':
972 val = '-'+self.sort[1]
973 else:
974 val = self.sort[1]
975 l.append(s%(':sort', val))
976 if group and self.group is not None:
977 if self.group[0] == '-':
978 val = '-'+self.group[1]
979 else:
980 val = self.group[1]
981 l.append(s%(':group', val))
982 if filter and self.filter:
983 l.append(s%(':filter', ','.join(self.filter)))
984 if filterspec:
985 for k,v in self.filterspec.items():
986 l.append(s%(k, ','.join(v)))
987 return '\n'.join(l)
989 def indexargs_href(self, url, args):
990 ''' embed the current index args in a URL '''
991 l = ['%s=%s'%(k,v) for k,v in args.items()]
992 if self.columns:
993 l.append(':columns=%s'%(','.join(self.columns.keys())))
994 if self.sort is not None:
995 if self.sort[0] == '-':
996 val = '-'+self.sort[1]
997 else:
998 val = self.sort[1]
999 l.append(':sort=%s'%val)
1000 if self.group is not None:
1001 if self.group[0] == '-':
1002 val = '-'+self.group[1]
1003 else:
1004 val = self.group[1]
1005 l.append(':group=%s'%val)
1006 if self.filter:
1007 l.append(':filter=%s'%(','.join(self.filter)))
1008 for k,v in self.filterspec.items():
1009 l.append('%s=%s'%(k, ','.join(v)))
1010 return '%s?%s'%(url, '&'.join(l))
1012 def base_javascript(self):
1013 return '''
1014 <script language="javascript">
1015 submitted = false;
1016 function submit_once() {
1017 if (submitted) {
1018 alert("Your request is being processed.\\nPlease be patient.");
1019 return 0;
1020 }
1021 submitted = true;
1022 return 1;
1023 }
1025 function help_window(helpurl, width, height) {
1026 HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1027 }
1028 </script>
1029 '''%self.base
1031 def batch(self):
1032 ''' Return a batch object for results from the "current search"
1033 '''
1034 filterspec = self.filterspec
1035 sort = self.sort
1036 group = self.group
1038 # get the list of ids we're batching over
1039 klass = self.client.db.getclass(self.classname)
1040 if self.search_text:
1041 matches = self.client.db.indexer.search(
1042 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1043 else:
1044 matches = None
1045 l = klass.filter(matches, filterspec, sort, group)
1047 # figure batch args
1048 if self.form.has_key(':pagesize'):
1049 size = int(self.form[':pagesize'].value)
1050 else:
1051 size = 50
1052 if self.form.has_key(':startwith'):
1053 start = int(self.form[':startwith'].value)
1054 else:
1055 start = 0
1057 # return the batch object
1058 return Batch(self.client, self.classname, l, size, start)
1061 # extend the standard ZTUtils Batch object to remove dependency on
1062 # Acquisition and add a couple of useful methods
1063 class Batch(ZTUtils.Batch):
1064 def __init__(self, client, classname, l, size, start, end=0, orphan=0, overlap=0):
1065 self.client = client
1066 self.classname = classname
1067 self.last_index = self.last_item = None
1068 self.current_item = None
1069 ZTUtils.Batch.__init__(self, l, size, start, end, orphan, overlap)
1071 # overwrite so we can late-instantiate the HTMLItem instance
1072 def __getitem__(self, index):
1073 if index < 0:
1074 if index + self.end < self.first: raise IndexError, index
1075 return self._sequence[index + self.end]
1077 if index >= self.length: raise IndexError, index
1079 # move the last_item along - but only if the fetched index changes
1080 # (for some reason, index 0 is fetched twice)
1081 if index != self.last_index:
1082 self.last_item = self.current_item
1083 self.last_index = index
1085 # wrap the return in an HTMLItem
1086 self.current_item = HTMLItem(self.client.db, self.classname,
1087 self._sequence[index+self.first])
1088 return self.current_item
1090 def propchanged(self, property):
1091 ''' Detect if the property marked as being the group property
1092 changed in the last iteration fetch
1093 '''
1094 if (self.last_item is None or
1095 self.last_item[property] != self.current_item[property]):
1096 return 1
1097 return 0
1099 # override these 'cos we don't have access to acquisition
1100 def previous(self):
1101 if self.start == 1:
1102 return None
1103 return Batch(self.client, self.classname, self._sequence, self._size,
1104 self.first - self._size + self.overlap, 0, self.orphan,
1105 self.overlap)
1107 def next(self):
1108 try:
1109 self._sequence[self.end]
1110 except IndexError:
1111 return None
1112 return Batch(self.client, self.classname, self._sequence, self._size,
1113 self.end - self.overlap, 0, self.orphan, self.overlap)
1115 def length(self):
1116 self.sequence_length = l = len(self._sequence)
1117 return l