1 import sys, cgi, urllib, os, re, os.path, time, errno
3 from roundup import hyperdb, date
4 from roundup.i18n import _
6 try:
7 import cPickle as pickle
8 except ImportError:
9 import pickle
10 try:
11 import cStringIO as StringIO
12 except ImportError:
13 import StringIO
14 try:
15 import StructuredText
16 except ImportError:
17 StructuredText = None
19 # bring in the templating support
20 from roundup.cgi.PageTemplates import PageTemplate
21 from roundup.cgi.PageTemplates.Expressions import getEngine
22 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
23 from roundup.cgi import ZTUtils
25 # XXX WAH pagetemplates aren't pickleable :(
26 #def getTemplate(dir, name, classname=None, request=None):
27 # ''' Interface to get a template, possibly loading a compiled template.
28 # '''
29 # # source
30 # src = os.path.join(dir, name)
31 #
32 # # see if we can get a compile from the template"c" directory (most
33 # # likely is "htmlc"
34 # split = list(os.path.split(dir))
35 # split[-1] = split[-1] + 'c'
36 # cdir = os.path.join(*split)
37 # split.append(name)
38 # cpl = os.path.join(*split)
39 #
40 # # ok, now see if the source is newer than the compiled (or if the
41 # # compiled even exists)
42 # MTIME = os.path.stat.ST_MTIME
43 # if (not os.path.exists(cpl) or os.stat(cpl)[MTIME] < os.stat(src)[MTIME]):
44 # # nope, we need to compile
45 # pt = RoundupPageTemplate()
46 # pt.write(open(src).read())
47 # pt.id = name
48 #
49 # # save off the compiled template
50 # if not os.path.exists(cdir):
51 # os.makedirs(cdir)
52 # f = open(cpl, 'wb')
53 # pickle.dump(pt, f)
54 # f.close()
55 # else:
56 # # yay, use the compiled template
57 # f = open(cpl, 'rb')
58 # pt = pickle.load(f)
59 # return pt
61 templates = {}
63 class NoTemplate(Exception):
64 pass
66 def getTemplate(dir, name, extension, classname=None, request=None):
67 ''' Interface to get a template, possibly loading a compiled template.
69 "name" and "extension" indicate the template we're after, which in
70 most cases will be "name.extension". If "extension" is None, then
71 we look for a template just called "name" with no extension.
73 If the file "name.extension" doesn't exist, we look for
74 "_generic.extension" as a fallback.
75 '''
76 # default the name to "home"
77 if name is None:
78 name = 'home'
80 # find the source, figure the time it was last modified
81 if extension:
82 filename = '%s.%s'%(name, extension)
83 else:
84 filename = name
85 src = os.path.join(dir, filename)
86 try:
87 stime = os.stat(src)[os.path.stat.ST_MTIME]
88 except os.error, error:
89 if error.errno != errno.ENOENT:
90 raise
91 if not extension:
92 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
94 # try for a generic template
95 generic = '_generic.%s'%extension
96 src = os.path.join(dir, generic)
97 try:
98 stime = os.stat(src)[os.path.stat.ST_MTIME]
99 except os.error, error:
100 if error.errno != errno.ENOENT:
101 raise
102 # nicer error
103 raise NoTemplate, 'No template file exists for templating '\
104 '"%s" with template "%s" (neither "%s" nor "%s")'%(name,
105 extension, filename, generic)
106 filename = generic
108 key = (dir, filename)
109 if templates.has_key(key) and stime < templates[key].mtime:
110 # compiled template is up to date
111 return templates[key]
113 # compile the template
114 templates[key] = pt = RoundupPageTemplate()
115 pt.write(open(src).read())
116 pt.id = filename
117 pt.mtime = time.time()
118 return pt
120 class RoundupPageTemplate(PageTemplate.PageTemplate):
121 ''' A Roundup-specific PageTemplate.
123 Interrogate the client to set up the various template variables to
124 be available:
126 *context*
127 this is one of three things:
128 1. None - we're viewing a "home" page
129 2. The current class of item being displayed. This is an HTMLClass
130 instance.
131 3. The current item from the database, if we're viewing a specific
132 item, as an HTMLItem instance.
133 *request*
134 Includes information about the current request, including:
135 - the url
136 - the current index information (``filterspec``, ``filter`` args,
137 ``properties``, etc) parsed out of the form.
138 - methods for easy filterspec link generation
139 - *user*, the current user node as an HTMLItem instance
140 - *form*, the current CGI form information as a FieldStorage
141 *instance*
142 The current instance
143 *db*
144 The current database, through which db.config may be reached.
145 '''
146 def getContext(self, client, classname, request):
147 c = {
148 'options': {},
149 'nothing': None,
150 'request': request,
151 'content': client.content,
152 'db': HTMLDatabase(client),
153 'instance': client.instance
154 }
155 # add in the item if there is one
156 if client.nodeid:
157 c['context'] = HTMLItem(client, classname, client.nodeid)
158 else:
159 c['context'] = HTMLClass(client, classname)
160 return c
162 def render(self, client, classname, request, **options):
163 """Render this Page Template"""
165 if not self._v_cooked:
166 self._cook()
168 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
170 if self._v_errors:
171 raise PageTemplate.PTRuntimeError, \
172 'Page Template %s has errors.' % self.id
174 # figure the context
175 classname = classname or client.classname
176 request = request or HTMLRequest(client)
177 c = self.getContext(client, classname, request)
178 c.update({'options': options})
180 # and go
181 output = StringIO.StringIO()
182 TALInterpreter(self._v_program, self._v_macros,
183 getEngine().getContext(c), output, tal=1, strictinsert=0)()
184 return output.getvalue()
186 class HTMLDatabase:
187 ''' Return HTMLClasses for valid class fetches
188 '''
189 def __init__(self, client):
190 self._client = client
192 # we want config to be exposed
193 self.config = client.db.config
195 def __getattr__(self, attr):
196 try:
197 self._client.db.getclass(attr)
198 except KeyError:
199 raise AttributeError, attr
200 return HTMLClass(self._client, attr)
201 def classes(self):
202 l = self._client.db.classes.keys()
203 l.sort()
204 return [HTMLClass(self._client, cn) for cn in l]
206 class HTMLClass:
207 ''' Accesses through a class (either through *class* or *db.<classname>*)
208 '''
209 def __init__(self, client, classname):
210 self._client = client
211 self._db = client.db
213 # we want classname to be exposed
214 self.classname = classname
215 if classname is not None:
216 self._klass = self._db.getclass(self.classname)
217 self._props = self._klass.getprops()
219 def __repr__(self):
220 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
222 def __getitem__(self, item):
223 ''' return an HTMLProperty instance
224 '''
225 #print 'getitem', (self, item)
227 # we don't exist
228 if item == 'id':
229 return None
230 if item == 'creator':
231 # but we will be created by this user...
232 return HTMLUser(self._client, 'user', self._client.userid)
234 # get the property
235 prop = self._props[item]
237 # look up the correct HTMLProperty class
238 for klass, htmlklass in propclasses:
239 if isinstance(prop, hyperdb.Multilink):
240 value = []
241 else:
242 value = None
243 if isinstance(prop, klass):
244 return htmlklass(self._client, '', prop, item, value)
246 # no good
247 raise KeyError, item
249 def __getattr__(self, attr):
250 ''' convenience access '''
251 try:
252 return self[attr]
253 except KeyError:
254 raise AttributeError, attr
256 def properties(self):
257 ''' Return HTMLProperty for all props
258 '''
259 l = []
260 for name, prop in self._props.items():
261 for klass, htmlklass in propclasses:
262 if isinstance(prop, hyperdb.Multilink):
263 value = []
264 else:
265 value = None
266 if isinstance(prop, klass):
267 l.append(htmlklass(self._client, '', prop, name, value))
268 return l
270 def list(self):
271 if self.classname == 'user':
272 klass = HTMLUser
273 else:
274 klass = HTMLItem
275 l = [klass(self._client, self.classname, x) for x in self._klass.list()]
276 return l
278 def csv(self):
279 ''' Return the items of this class as a chunk of CSV text.
280 '''
281 # get the CSV module
282 try:
283 import csv
284 except ImportError:
285 return 'Sorry, you need the csv module to use this function.\n'\
286 'Get it from: http://www.object-craft.com.au/projects/csv/'
288 props = self.propnames()
289 p = csv.parser()
290 s = StringIO.StringIO()
291 s.write(p.join(props) + '\n')
292 for nodeid in self._klass.list():
293 l = []
294 for name in props:
295 value = self._klass.get(nodeid, name)
296 if value is None:
297 l.append('')
298 elif isinstance(value, type([])):
299 l.append(':'.join(map(str, value)))
300 else:
301 l.append(str(self._klass.get(nodeid, name)))
302 s.write(p.join(l) + '\n')
303 return s.getvalue()
305 def propnames(self):
306 ''' Return the list of the names of the properties of this class.
307 '''
308 idlessprops = self._klass.getprops(protected=0).keys()
309 idlessprops.sort()
310 return ['id'] + idlessprops
312 def filter(self, request=None):
313 ''' Return a list of items from this class, filtered and sorted
314 by the current requested filterspec/filter/sort/group args
315 '''
316 if request is not None:
317 filterspec = request.filterspec
318 sort = request.sort
319 group = request.group
320 if self.classname == 'user':
321 klass = HTMLUser
322 else:
323 klass = HTMLItem
324 l = [klass(self._client, self.classname, x)
325 for x in self._klass.filter(None, filterspec, sort, group)]
326 return l
328 def classhelp(self, properties, label='?', width='400', height='400'):
329 '''pop up a javascript window with class help
331 This generates a link to a popup window which displays the
332 properties indicated by "properties" of the class named by
333 "classname". The "properties" should be a comma-separated list
334 (eg. 'id,name,description').
336 You may optionally override the label displayed, the width and
337 height. The popup window will be resizable and scrollable.
338 '''
339 return '<a href="javascript:help_window(\'%s?:template=help&' \
340 ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
341 '(%s)</b></a>'%(self.classname, properties, width, height, label)
343 def submit(self, label="Submit New Entry"):
344 ''' Generate a submit button (and action hidden element)
345 '''
346 return ' <input type="hidden" name=":action" value="new">\n'\
347 ' <input type="submit" name="submit" value="%s">'%label
349 def history(self):
350 return 'New node - no history'
352 def renderWith(self, name, **kwargs):
353 ''' Render this class with the given template.
354 '''
355 # create a new request and override the specified args
356 req = HTMLRequest(self._client)
357 req.classname = self.classname
358 req.update(kwargs)
360 # new template, using the specified classname and request
361 pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
363 # XXX handle PT rendering errors here nicely
364 try:
365 # use our fabricated request
366 return pt.render(self._client, self.classname, req)
367 except PageTemplate.PTRuntimeError, message:
368 return '<strong>%s</strong><ol>%s</ol>'%(message,
369 cgi.escape('<li>'.join(pt._v_errors)))
371 class HTMLItem:
372 ''' Accesses through an *item*
373 '''
374 def __init__(self, client, classname, nodeid):
375 self._client = client
376 self._db = client.db
377 self._classname = classname
378 self._nodeid = nodeid
379 self._klass = self._db.getclass(classname)
380 self._props = self._klass.getprops()
382 def __repr__(self):
383 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
384 self._nodeid)
386 def __getitem__(self, item):
387 ''' return an HTMLProperty instance
388 '''
389 #print 'getitem', (self, item)
390 if item == 'id':
391 return self._nodeid
393 # get the property
394 prop = self._props[item]
396 # get the value, handling missing values
397 value = self._klass.get(self._nodeid, item, None)
398 if value is None:
399 if isinstance(self._props[item], hyperdb.Multilink):
400 value = []
402 # look up the correct HTMLProperty class
403 for klass, htmlklass in propclasses:
404 if isinstance(prop, klass):
405 return htmlklass(self._client, self._nodeid, prop, item, value)
407 raise KeyErorr, item
409 def __getattr__(self, attr):
410 ''' convenience access to properties '''
411 try:
412 return self[attr]
413 except KeyError:
414 raise AttributeError, attr
416 def submit(self, label="Submit Changes"):
417 ''' Generate a submit button (and action hidden element)
418 '''
419 return ' <input type="hidden" name=":action" value="edit">\n'\
420 ' <input type="submit" name="submit" value="%s">'%label
422 # XXX this probably should just return the history items, not the HTML
423 def history(self, direction='descending'):
424 l = ['<table class="history">'
425 '<tr><th colspan="4" class="header">',
426 _('History'),
427 '</th></tr><tr>',
428 _('<th>Date</th>'),
429 _('<th>User</th>'),
430 _('<th>Action</th>'),
431 _('<th>Args</th>'),
432 '</tr>']
433 comments = {}
434 history = self._klass.history(self._nodeid)
435 history.sort()
436 if direction == 'descending':
437 history.reverse()
438 for id, evt_date, user, action, args in history:
439 date_s = str(evt_date).replace("."," ")
440 arg_s = ''
441 if action == 'link' and type(args) == type(()):
442 if len(args) == 3:
443 linkcl, linkid, key = args
444 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
445 linkcl, linkid, key)
446 else:
447 arg_s = str(args)
449 elif action == 'unlink' and type(args) == type(()):
450 if len(args) == 3:
451 linkcl, linkid, key = args
452 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
453 linkcl, linkid, key)
454 else:
455 arg_s = str(args)
457 elif type(args) == type({}):
458 cell = []
459 for k in args.keys():
460 # try to get the relevant property and treat it
461 # specially
462 try:
463 prop = self._props[k]
464 except KeyError:
465 prop = None
466 if prop is not None:
467 if args[k] and (isinstance(prop, hyperdb.Multilink) or
468 isinstance(prop, hyperdb.Link)):
469 # figure what the link class is
470 classname = prop.classname
471 try:
472 linkcl = self._db.getclass(classname)
473 except KeyError:
474 labelprop = None
475 comments[classname] = _('''The linked class
476 %(classname)s no longer exists''')%locals()
477 labelprop = linkcl.labelprop(1)
478 hrefable = os.path.exists(
479 os.path.join(self._db.config.TEMPLATES,
480 classname+'.item'))
482 if isinstance(prop, hyperdb.Multilink) and \
483 len(args[k]) > 0:
484 ml = []
485 for linkid in args[k]:
486 if isinstance(linkid, type(())):
487 sublabel = linkid[0] + ' '
488 linkids = linkid[1]
489 else:
490 sublabel = ''
491 linkids = [linkid]
492 subml = []
493 for linkid in linkids:
494 label = classname + linkid
495 # if we have a label property, try to use it
496 # TODO: test for node existence even when
497 # there's no labelprop!
498 try:
499 if labelprop is not None:
500 label = linkcl.get(linkid, labelprop)
501 except IndexError:
502 comments['no_link'] = _('''<strike>The
503 linked node no longer
504 exists</strike>''')
505 subml.append('<strike>%s</strike>'%label)
506 else:
507 if hrefable:
508 subml.append('<a href="%s%s">%s</a>'%(
509 classname, linkid, label))
510 ml.append(sublabel + ', '.join(subml))
511 cell.append('%s:\n %s'%(k, ', '.join(ml)))
512 elif isinstance(prop, hyperdb.Link) and args[k]:
513 label = classname + args[k]
514 # if we have a label property, try to use it
515 # TODO: test for node existence even when
516 # there's no labelprop!
517 if labelprop is not None:
518 try:
519 label = linkcl.get(args[k], labelprop)
520 except IndexError:
521 comments['no_link'] = _('''<strike>The
522 linked node no longer
523 exists</strike>''')
524 cell.append(' <strike>%s</strike>,\n'%label)
525 # "flag" this is done .... euwww
526 label = None
527 if label is not None:
528 if hrefable:
529 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
530 classname, args[k], label))
531 else:
532 cell.append('%s: %s' % (k,label))
534 elif isinstance(prop, hyperdb.Date) and args[k]:
535 d = date.Date(args[k])
536 cell.append('%s: %s'%(k, str(d)))
538 elif isinstance(prop, hyperdb.Interval) and args[k]:
539 d = date.Interval(args[k])
540 cell.append('%s: %s'%(k, str(d)))
542 elif isinstance(prop, hyperdb.String) and args[k]:
543 cell.append('%s: %s'%(k, cgi.escape(args[k])))
545 elif not args[k]:
546 cell.append('%s: (no value)\n'%k)
548 else:
549 cell.append('%s: %s\n'%(k, str(args[k])))
550 else:
551 # property no longer exists
552 comments['no_exist'] = _('''<em>The indicated property
553 no longer exists</em>''')
554 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
555 arg_s = '<br />'.join(cell)
556 else:
557 # unkown event!!
558 comments['unknown'] = _('''<strong><em>This event is not
559 handled by the history display!</em></strong>''')
560 arg_s = '<strong><em>' + str(args) + '</em></strong>'
561 date_s = date_s.replace(' ', ' ')
562 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
563 date_s, user, action, arg_s))
564 if comments:
565 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
566 for entry in comments.values():
567 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
568 l.append('</table>')
569 return '\n'.join(l)
571 class HTMLUser(HTMLItem):
572 ''' Accesses through the *user* (a special case of item)
573 '''
574 def __init__(self, client, classname, nodeid):
575 HTMLItem.__init__(self, client, 'user', nodeid)
576 self._default_classname = client.classname
578 # used for security checks
579 self._security = client.db.security
580 _marker = []
581 def hasPermission(self, role, classname=_marker):
582 ''' Determine if the user has the Role.
584 The class being tested defaults to the template's class, but may
585 be overidden for this test by suppling an alternate classname.
586 '''
587 if classname is self._marker:
588 classname = self._default_classname
589 return self._security.hasPermission(role, self._nodeid, classname)
591 class HTMLProperty:
592 ''' String, Number, Date, Interval HTMLProperty
594 Hase useful attributes:
596 _name the name of the property
597 _value the value of the property if any
599 A wrapper object which may be stringified for the plain() behaviour.
600 '''
601 def __init__(self, client, nodeid, prop, name, value):
602 self._client = client
603 self._db = client.db
604 self._nodeid = nodeid
605 self._prop = prop
606 self._name = name
607 self._value = value
608 def __repr__(self):
609 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
610 def __str__(self):
611 return self.plain()
612 def __cmp__(self, other):
613 if isinstance(other, HTMLProperty):
614 return cmp(self._value, other._value)
615 return cmp(self._value, other)
617 class StringHTMLProperty(HTMLProperty):
618 def plain(self, escape=0):
619 if self._value is None:
620 return ''
621 if escape:
622 return cgi.escape(str(self._value))
623 return str(self._value)
625 def stext(self, escape=0):
626 s = self.plain(escape=escape)
627 if not StructuredText:
628 return s
629 return StructuredText(s,level=1,header=0)
631 def field(self, size = 30):
632 if self._value is None:
633 value = ''
634 else:
635 value = cgi.escape(str(self._value))
636 value = '"'.join(value.split('"'))
637 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
639 def multiline(self, escape=0, rows=5, cols=40):
640 if self._value is None:
641 value = ''
642 else:
643 value = cgi.escape(str(self._value))
644 value = '"'.join(value.split('"'))
645 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
646 self._name, rows, cols, value)
648 def email(self, escape=1):
649 ''' fudge email '''
650 if self._value is None: value = ''
651 else: value = str(self._value)
652 value = value.replace('@', ' at ')
653 value = value.replace('.', ' ')
654 if escape:
655 value = cgi.escape(value)
656 return value
658 class PasswordHTMLProperty(HTMLProperty):
659 def plain(self):
660 if self._value is None:
661 return ''
662 return _('*encrypted*')
664 def field(self, size = 30):
665 return '<input type="password" name="%s" size="%s">'%(self._name, size)
667 class NumberHTMLProperty(HTMLProperty):
668 def plain(self):
669 return str(self._value)
671 def field(self, size = 30):
672 if self._value is None:
673 value = ''
674 else:
675 value = cgi.escape(str(self._value))
676 value = '"'.join(value.split('"'))
677 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
679 class BooleanHTMLProperty(HTMLProperty):
680 def plain(self):
681 if self.value is None:
682 return ''
683 return self._value and "Yes" or "No"
685 def field(self):
686 checked = self._value and "checked" or ""
687 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
688 checked)
689 if checked:
690 checked = ""
691 else:
692 checked = "checked"
693 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
694 checked)
695 return s
697 class DateHTMLProperty(HTMLProperty):
698 def plain(self):
699 if self._value is None:
700 return ''
701 return str(self._value)
703 def field(self, size = 30):
704 if self._value is None:
705 value = ''
706 else:
707 value = cgi.escape(str(self._value))
708 value = '"'.join(value.split('"'))
709 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
711 def reldate(self, pretty=1):
712 if not self._value:
713 return ''
715 # figure the interval
716 interval = date.Date('.') - self._value
717 if pretty:
718 return interval.pretty()
719 return str(interval)
721 class IntervalHTMLProperty(HTMLProperty):
722 def plain(self):
723 if self._value is None:
724 return ''
725 return str(self._value)
727 def pretty(self):
728 return self._value.pretty()
730 def field(self, size = 30):
731 if self._value is None:
732 value = ''
733 else:
734 value = cgi.escape(str(self._value))
735 value = '"'.join(value.split('"'))
736 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
738 class LinkHTMLProperty(HTMLProperty):
739 ''' Link HTMLProperty
740 Include the above as well as being able to access the class
741 information. Stringifying the object itself results in the value
742 from the item being displayed. Accessing attributes of this object
743 result in the appropriate entry from the class being queried for the
744 property accessed (so item/assignedto/name would look up the user
745 entry identified by the assignedto property on item, and then the
746 name property of that user)
747 '''
748 def __getattr__(self, attr):
749 ''' return a new HTMLItem '''
750 #print 'getattr', (self, attr, self._value)
751 if not self._value:
752 raise AttributeError, "Can't access missing value"
753 if self._prop.classname == 'user':
754 klass = HTMLItem
755 else:
756 klass = HTMLUser
757 i = klass(self._client, self._prop.classname, self._value)
758 return getattr(i, attr)
760 def plain(self, escape=0):
761 if self._value is None:
762 return _('[unselected]')
763 linkcl = self._db.classes[self._prop.classname]
764 k = linkcl.labelprop(1)
765 value = str(linkcl.get(self._value, k))
766 if escape:
767 value = cgi.escape(value)
768 return value
770 def field(self):
771 linkcl = self._db.getclass(self._prop.classname)
772 if linkcl.getprops().has_key('order'):
773 sort_on = 'order'
774 else:
775 sort_on = linkcl.labelprop()
776 options = linkcl.filter(None, {}, [sort_on], [])
777 # TODO: make this a field display, not a menu one!
778 l = ['<select name="%s">'%property]
779 k = linkcl.labelprop(1)
780 if value is None:
781 s = 'selected '
782 else:
783 s = ''
784 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
785 for optionid in options:
786 option = linkcl.get(optionid, k)
787 s = ''
788 if optionid == value:
789 s = 'selected '
790 if showid:
791 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
792 else:
793 lab = option
794 if size is not None and len(lab) > size:
795 lab = lab[:size-3] + '...'
796 lab = cgi.escape(lab)
797 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
798 l.append('</select>')
799 return '\n'.join(l)
801 def download(self, showid=0):
802 linkname = self._prop.classname
803 linkcl = self._db.getclass(linkname)
804 k = linkcl.labelprop(1)
805 linkvalue = cgi.escape(str(linkcl.get(self._value, k)))
806 if showid:
807 label = value
808 title = ' title="%s"'%linkvalue
809 # note ... this should be urllib.quote(linkcl.get(value, k))
810 else:
811 label = linkvalue
812 title = ''
813 return '<a href="%s%s/%s"%s>%s</a>'%(linkname, self._value,
814 linkvalue, title, label)
816 def menu(self, size=None, height=None, showid=0, additional=[],
817 **conditions):
818 value = self._value
820 # sort function
821 sortfunc = make_sort_function(self._db, self._prop.classname)
823 # force the value to be a single choice
824 if isinstance(value, type('')):
825 value = value[0]
826 linkcl = self._db.getclass(self._prop.classname)
827 l = ['<select name="%s">'%self._name]
828 k = linkcl.labelprop(1)
829 s = ''
830 if value is None:
831 s = 'selected '
832 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
833 if linkcl.getprops().has_key('order'):
834 sort_on = ('+', 'order')
835 else:
836 sort_on = ('+', linkcl.labelprop())
837 options = linkcl.filter(None, conditions, sort_on, (None, None))
838 for optionid in options:
839 option = linkcl.get(optionid, k)
840 s = ''
841 if value in [optionid, option]:
842 s = 'selected '
843 if showid:
844 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
845 else:
846 lab = option
847 if size is not None and len(lab) > size:
848 lab = lab[:size-3] + '...'
849 if additional:
850 m = []
851 for propname in additional:
852 m.append(linkcl.get(optionid, propname))
853 lab = lab + ' (%s)'%', '.join(map(str, m))
854 lab = cgi.escape(lab)
855 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
856 l.append('</select>')
857 return '\n'.join(l)
859 # def checklist(self, ...)
861 class MultilinkHTMLProperty(HTMLProperty):
862 ''' Multilink HTMLProperty
864 Also be iterable, returning a wrapper object like the Link case for
865 each entry in the multilink.
866 '''
867 def __len__(self):
868 ''' length of the multilink '''
869 return len(self._value)
871 def __getattr__(self, attr):
872 ''' no extended attribute accesses make sense here '''
873 raise AttributeError, attr
875 def __getitem__(self, num):
876 ''' iterate and return a new HTMLItem
877 '''
878 #print 'getitem', (self, num)
879 value = self._value[num]
880 if self._prop.classname == 'user':
881 klass = HTMLUser
882 else:
883 klass = HTMLItem
884 return klass(self._client, self._prop.classname, value)
886 def reverse(self):
887 ''' return the list in reverse order
888 '''
889 l = self._value[:]
890 l.reverse()
891 if self._prop.classname == 'user':
892 klass = HTMLUser
893 else:
894 klass = HTMLItem
895 return [klass(self._client, self._prop.classname, value) for value in l]
897 def plain(self, escape=0):
898 linkcl = self._db.classes[self._prop.classname]
899 k = linkcl.labelprop(1)
900 labels = []
901 for v in self._value:
902 labels.append(linkcl.get(v, k))
903 value = ', '.join(labels)
904 if escape:
905 value = cgi.escape(value)
906 return value
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.TRACKER_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