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