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