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