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 'utils': TemplatingUtils(client),
155 }
156 # add in the item if there is one
157 if client.nodeid:
158 c['context'] = HTMLItem(client, classname, client.nodeid)
159 else:
160 c['context'] = HTMLClass(client, classname)
161 return c
163 def render(self, client, classname, request, **options):
164 """Render this Page Template"""
166 if not self._v_cooked:
167 self._cook()
169 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
171 if self._v_errors:
172 raise PageTemplate.PTRuntimeError, \
173 'Page Template %s has errors.' % self.id
175 # figure the context
176 classname = classname or client.classname
177 request = request or HTMLRequest(client)
178 c = self.getContext(client, classname, request)
179 c.update({'options': options})
181 # and go
182 output = StringIO.StringIO()
183 TALInterpreter(self._v_program, self._v_macros,
184 getEngine().getContext(c), output, tal=1, strictinsert=0)()
185 return output.getvalue()
187 class HTMLDatabase:
188 ''' Return HTMLClasses for valid class fetches
189 '''
190 def __init__(self, client):
191 self._client = client
193 # we want config to be exposed
194 self.config = client.db.config
196 def __getitem__(self, item):
197 self._client.db.getclass(item)
198 return HTMLClass(self._client, item)
200 def __getattr__(self, attr):
201 try:
202 return self[attr]
203 except KeyError:
204 raise AttributeError, attr
206 def classes(self):
207 l = self._client.db.classes.keys()
208 l.sort()
209 return [HTMLClass(self._client, cn) for cn in l]
211 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
212 cl = db.getclass(prop.classname)
213 l = []
214 for entry in ids:
215 if num_re.match(entry):
216 l.append(entry)
217 else:
218 l.append(cl.lookup(entry))
219 return l
221 class HTMLClass:
222 ''' Accesses through a class (either through *class* or *db.<classname>*)
223 '''
224 def __init__(self, client, classname):
225 self._client = client
226 self._db = client.db
228 # we want classname to be exposed
229 self.classname = classname
230 if classname is not None:
231 self._klass = self._db.getclass(self.classname)
232 self._props = self._klass.getprops()
234 def __repr__(self):
235 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
237 def __getitem__(self, item):
238 ''' return an HTMLProperty instance
239 '''
240 #print 'HTMLClass.getitem', (self, item)
242 # we don't exist
243 if item == 'id':
244 return None
246 # get the property
247 prop = self._props[item]
249 # look up the correct HTMLProperty class
250 form = self._client.form
251 for klass, htmlklass in propclasses:
252 if not isinstance(prop, klass):
253 continue
254 if form.has_key(item):
255 if isinstance(prop, hyperdb.Multilink):
256 value = lookupIds(self._db, prop,
257 handleListCGIValue(form[item]))
258 elif isinstance(prop, hyperdb.Link):
259 value = form[item].value.strip()
260 if value:
261 value = lookupIds(self._db, prop, [value])[0]
262 else:
263 value = None
264 else:
265 value = form[item].value.strip() or None
266 else:
267 if isinstance(prop, hyperdb.Multilink):
268 value = []
269 else:
270 value = None
271 return htmlklass(self._client, '', prop, item, value)
273 # no good
274 raise KeyError, item
276 def __getattr__(self, attr):
277 ''' convenience access '''
278 try:
279 return self[attr]
280 except KeyError:
281 raise AttributeError, attr
283 def properties(self):
284 ''' Return HTMLProperty for all of this class' properties.
285 '''
286 l = []
287 for name, prop in self._props.items():
288 for klass, htmlklass in propclasses:
289 if isinstance(prop, hyperdb.Multilink):
290 value = []
291 else:
292 value = None
293 if isinstance(prop, klass):
294 l.append(htmlklass(self._client, '', prop, name, value))
295 return l
297 def list(self):
298 ''' List all items in this class.
299 '''
300 if self.classname == 'user':
301 klass = HTMLUser
302 else:
303 klass = HTMLItem
304 l = [klass(self._client, self.classname, x) for x in self._klass.list()]
305 return l
307 def csv(self):
308 ''' Return the items of this class as a chunk of CSV text.
309 '''
310 # get the CSV module
311 try:
312 import csv
313 except ImportError:
314 return 'Sorry, you need the csv module to use this function.\n'\
315 'Get it from: http://www.object-craft.com.au/projects/csv/'
317 props = self.propnames()
318 p = csv.parser()
319 s = StringIO.StringIO()
320 s.write(p.join(props) + '\n')
321 for nodeid in self._klass.list():
322 l = []
323 for name in props:
324 value = self._klass.get(nodeid, name)
325 if value is None:
326 l.append('')
327 elif isinstance(value, type([])):
328 l.append(':'.join(map(str, value)))
329 else:
330 l.append(str(self._klass.get(nodeid, name)))
331 s.write(p.join(l) + '\n')
332 return s.getvalue()
334 def propnames(self):
335 ''' Return the list of the names of the properties of this class.
336 '''
337 idlessprops = self._klass.getprops(protected=0).keys()
338 idlessprops.sort()
339 return ['id'] + idlessprops
341 def filter(self, request=None):
342 ''' Return a list of items from this class, filtered and sorted
343 by the current requested filterspec/filter/sort/group args
344 '''
345 if request is not None:
346 filterspec = request.filterspec
347 sort = request.sort
348 group = request.group
349 if self.classname == 'user':
350 klass = HTMLUser
351 else:
352 klass = HTMLItem
353 l = [klass(self._client, self.classname, x)
354 for x in self._klass.filter(None, filterspec, sort, group)]
355 return l
357 def classhelp(self, properties=None, label='list', width='500',
358 height='400'):
359 ''' Pop up a javascript window with class help
361 This generates a link to a popup window which displays the
362 properties indicated by "properties" of the class named by
363 "classname". The "properties" should be a comma-separated list
364 (eg. 'id,name,description'). Properties defaults to all the
365 properties of a class (excluding id, creator, created and
366 activity).
368 You may optionally override the label displayed, the width and
369 height. The popup window will be resizable and scrollable.
370 '''
371 if properties is None:
372 properties = self._klass.getprops(protected=0).keys()
373 properties.sort()
374 properties = ','.join(properties)
375 return '<a href="javascript:help_window(\'%s?:template=help&' \
376 ':contentonly=1&properties=%s\', \'%s\', \'%s\')"><b>'\
377 '(%s)</b></a>'%(self.classname, properties, width, height, label)
379 def submit(self, label="Submit New Entry"):
380 ''' Generate a submit button (and action hidden element)
381 '''
382 return ' <input type="hidden" name=":action" value="new">\n'\
383 ' <input type="submit" name="submit" value="%s">'%label
385 def history(self):
386 return 'New node - no history'
388 def renderWith(self, name, **kwargs):
389 ''' Render this class with the given template.
390 '''
391 # create a new request and override the specified args
392 req = HTMLRequest(self._client)
393 req.classname = self.classname
394 req.update(kwargs)
396 # new template, using the specified classname and request
397 pt = getTemplate(self._db.config.TEMPLATES, self.classname, name)
399 # use our fabricated request
400 return pt.render(self._client, self.classname, req)
402 class HTMLItem:
403 ''' Accesses through an *item*
404 '''
405 def __init__(self, client, classname, nodeid):
406 self._client = client
407 self._db = client.db
408 self._classname = classname
409 self._nodeid = nodeid
410 self._klass = self._db.getclass(classname)
411 self._props = self._klass.getprops()
413 def __repr__(self):
414 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
415 self._nodeid)
417 def __getitem__(self, item):
418 ''' return an HTMLProperty instance
419 '''
420 #print 'HTMLItem.getitem', (self, item)
421 if item == 'id':
422 return self._nodeid
424 # get the property
425 prop = self._props[item]
427 # get the value, handling missing values
428 value = self._klass.get(self._nodeid, item, None)
429 if value is None:
430 if isinstance(self._props[item], hyperdb.Multilink):
431 value = []
433 # look up the correct HTMLProperty class
434 for klass, htmlklass in propclasses:
435 if isinstance(prop, klass):
436 return htmlklass(self._client, self._nodeid, prop, item, value)
438 raise KeyErorr, item
440 def __getattr__(self, attr):
441 ''' convenience access to properties '''
442 try:
443 return self[attr]
444 except KeyError:
445 raise AttributeError, attr
447 def submit(self, label="Submit Changes"):
448 ''' Generate a submit button (and action hidden element)
449 '''
450 return ' <input type="hidden" name=":action" value="edit">\n'\
451 ' <input type="submit" name="submit" value="%s">'%label
453 def journal(self, direction='descending'):
454 ''' Return a list of HTMLJournalEntry instances.
455 '''
456 # XXX do this
457 return []
459 def history(self, direction='descending'):
460 l = ['<table class="history">'
461 '<tr><th colspan="4" class="header">',
462 _('History'),
463 '</th></tr><tr>',
464 _('<th>Date</th>'),
465 _('<th>User</th>'),
466 _('<th>Action</th>'),
467 _('<th>Args</th>'),
468 '</tr>']
469 comments = {}
470 history = self._klass.history(self._nodeid)
471 history.sort()
472 if direction == 'descending':
473 history.reverse()
474 for id, evt_date, user, action, args in history:
475 date_s = str(evt_date).replace("."," ")
476 arg_s = ''
477 if action == 'link' and type(args) == type(()):
478 if len(args) == 3:
479 linkcl, linkid, key = args
480 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
481 linkcl, linkid, key)
482 else:
483 arg_s = str(args)
485 elif action == 'unlink' and type(args) == type(()):
486 if len(args) == 3:
487 linkcl, linkid, key = args
488 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
489 linkcl, linkid, key)
490 else:
491 arg_s = str(args)
493 elif type(args) == type({}):
494 cell = []
495 for k in args.keys():
496 # try to get the relevant property and treat it
497 # specially
498 try:
499 prop = self._props[k]
500 except KeyError:
501 prop = None
502 if prop is not None:
503 if args[k] and (isinstance(prop, hyperdb.Multilink) or
504 isinstance(prop, hyperdb.Link)):
505 # figure what the link class is
506 classname = prop.classname
507 try:
508 linkcl = self._db.getclass(classname)
509 except KeyError:
510 labelprop = None
511 comments[classname] = _('''The linked class
512 %(classname)s no longer exists''')%locals()
513 labelprop = linkcl.labelprop(1)
514 hrefable = os.path.exists(
515 os.path.join(self._db.config.TEMPLATES,
516 classname+'.item'))
518 if isinstance(prop, hyperdb.Multilink) and \
519 len(args[k]) > 0:
520 ml = []
521 for linkid in args[k]:
522 if isinstance(linkid, type(())):
523 sublabel = linkid[0] + ' '
524 linkids = linkid[1]
525 else:
526 sublabel = ''
527 linkids = [linkid]
528 subml = []
529 for linkid in linkids:
530 label = classname + linkid
531 # if we have a label property, try to use it
532 # TODO: test for node existence even when
533 # there's no labelprop!
534 try:
535 if labelprop is not None:
536 label = linkcl.get(linkid, labelprop)
537 except IndexError:
538 comments['no_link'] = _('''<strike>The
539 linked node no longer
540 exists</strike>''')
541 subml.append('<strike>%s</strike>'%label)
542 else:
543 if hrefable:
544 subml.append('<a href="%s%s">%s</a>'%(
545 classname, linkid, label))
546 ml.append(sublabel + ', '.join(subml))
547 cell.append('%s:\n %s'%(k, ', '.join(ml)))
548 elif isinstance(prop, hyperdb.Link) and args[k]:
549 label = classname + args[k]
550 # if we have a label property, try to use it
551 # TODO: test for node existence even when
552 # there's no labelprop!
553 if labelprop is not None:
554 try:
555 label = linkcl.get(args[k], labelprop)
556 except IndexError:
557 comments['no_link'] = _('''<strike>The
558 linked node no longer
559 exists</strike>''')
560 cell.append(' <strike>%s</strike>,\n'%label)
561 # "flag" this is done .... euwww
562 label = None
563 if label is not None:
564 if hrefable:
565 cell.append('%s: <a href="%s%s">%s</a>\n'%(k,
566 classname, args[k], label))
567 else:
568 cell.append('%s: %s' % (k,label))
570 elif isinstance(prop, hyperdb.Date) and args[k]:
571 d = date.Date(args[k])
572 cell.append('%s: %s'%(k, str(d)))
574 elif isinstance(prop, hyperdb.Interval) and args[k]:
575 d = date.Interval(args[k])
576 cell.append('%s: %s'%(k, str(d)))
578 elif isinstance(prop, hyperdb.String) and args[k]:
579 cell.append('%s: %s'%(k, cgi.escape(args[k])))
581 elif not args[k]:
582 cell.append('%s: (no value)\n'%k)
584 else:
585 cell.append('%s: %s\n'%(k, str(args[k])))
586 else:
587 # property no longer exists
588 comments['no_exist'] = _('''<em>The indicated property
589 no longer exists</em>''')
590 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
591 arg_s = '<br />'.join(cell)
592 else:
593 # unkown event!!
594 comments['unknown'] = _('''<strong><em>This event is not
595 handled by the history display!</em></strong>''')
596 arg_s = '<strong><em>' + str(args) + '</em></strong>'
597 date_s = date_s.replace(' ', ' ')
598 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
599 date_s, user, action, arg_s))
600 if comments:
601 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
602 for entry in comments.values():
603 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
604 l.append('</table>')
605 return '\n'.join(l)
607 def renderQueryForm(self):
608 ''' Render this item, which is a query, as a search form.
609 '''
610 # create a new request and override the specified args
611 req = HTMLRequest(self._client)
612 req.classname = self._klass.get(self._nodeid, 'klass')
613 req.updateFromURL(self._klass.get(self._nodeid, 'url'))
615 # new template, using the specified classname and request
616 pt = getTemplate(self._db.config.TEMPLATES, req.classname, 'search')
618 # use our fabricated request
619 return pt.render(self._client, req.classname, req)
621 class HTMLUser(HTMLItem):
622 ''' Accesses through the *user* (a special case of item)
623 '''
624 def __init__(self, client, classname, nodeid):
625 HTMLItem.__init__(self, client, 'user', nodeid)
626 self._default_classname = client.classname
628 # used for security checks
629 self._security = client.db.security
630 _marker = []
631 def hasPermission(self, role, classname=_marker):
632 ''' Determine if the user has the Role.
634 The class being tested defaults to the template's class, but may
635 be overidden for this test by suppling an alternate classname.
636 '''
637 if classname is self._marker:
638 classname = self._default_classname
639 return self._security.hasPermission(role, self._nodeid, classname)
641 class HTMLProperty:
642 ''' String, Number, Date, Interval HTMLProperty
644 Has useful attributes:
646 _name the name of the property
647 _value the value of the property if any
649 A wrapper object which may be stringified for the plain() behaviour.
650 '''
651 def __init__(self, client, nodeid, prop, name, value):
652 self._client = client
653 self._db = client.db
654 self._nodeid = nodeid
655 self._prop = prop
656 self._name = name
657 self._value = value
658 def __repr__(self):
659 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._name, self._prop, self._value)
660 def __str__(self):
661 return self.plain()
662 def __cmp__(self, other):
663 if isinstance(other, HTMLProperty):
664 return cmp(self._value, other._value)
665 return cmp(self._value, other)
667 class StringHTMLProperty(HTMLProperty):
668 def plain(self, escape=0):
669 ''' Render a "plain" representation of the property
670 '''
671 if self._value is None:
672 return ''
673 if escape:
674 return cgi.escape(str(self._value))
675 return str(self._value)
677 def stext(self, escape=0):
678 ''' Render the value of the property as StructuredText.
680 This requires the StructureText module to be installed separately.
681 '''
682 s = self.plain(escape=escape)
683 if not StructuredText:
684 return s
685 return StructuredText(s,level=1,header=0)
687 def field(self, size = 30):
688 ''' Render a form edit field for the property
689 '''
690 if self._value is None:
691 value = ''
692 else:
693 value = cgi.escape(str(self._value))
694 value = '"'.join(value.split('"'))
695 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
697 def multiline(self, escape=0, rows=5, cols=40):
698 ''' Render a multiline form edit field for the property
699 '''
700 if self._value is None:
701 value = ''
702 else:
703 value = cgi.escape(str(self._value))
704 value = '"'.join(value.split('"'))
705 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
706 self._name, rows, cols, value)
708 def email(self, escape=1):
709 ''' Render the value of the property as an obscured email address
710 '''
711 if self._value is None: value = ''
712 else: value = str(self._value)
713 if value.find('@') != -1:
714 name, domain = value.split('@')
715 domain = ' '.join(domain.split('.')[:-1])
716 name = name.replace('.', ' ')
717 value = '%s at %s ...'%(name, domain)
718 else:
719 value = value.replace('.', ' ')
720 if escape:
721 value = cgi.escape(value)
722 return value
724 class PasswordHTMLProperty(HTMLProperty):
725 def plain(self):
726 ''' Render a "plain" representation of the property
727 '''
728 if self._value is None:
729 return ''
730 return _('*encrypted*')
732 def field(self, size = 30):
733 ''' Render a form edit field for the property
734 '''
735 return '<input type="password" name="%s" size="%s">'%(self._name, size)
737 class NumberHTMLProperty(HTMLProperty):
738 def plain(self):
739 ''' Render a "plain" representation of the property
740 '''
741 return str(self._value)
743 def field(self, size = 30):
744 ''' Render a form edit field for the property
745 '''
746 if self._value is None:
747 value = ''
748 else:
749 value = cgi.escape(str(self._value))
750 value = '"'.join(value.split('"'))
751 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
753 class BooleanHTMLProperty(HTMLProperty):
754 def plain(self):
755 ''' Render a "plain" representation of the property
756 '''
757 if self.value is None:
758 return ''
759 return self._value and "Yes" or "No"
761 def field(self):
762 ''' Render a form edit field for the property
763 '''
764 checked = self._value and "checked" or ""
765 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._name,
766 checked)
767 if checked:
768 checked = ""
769 else:
770 checked = "checked"
771 s += '<input type="radio" name="%s" value="no" %s>No'%(self._name,
772 checked)
773 return s
775 class DateHTMLProperty(HTMLProperty):
776 def plain(self):
777 ''' Render a "plain" representation of the property
778 '''
779 if self._value is None:
780 return ''
781 return str(self._value)
783 def field(self, size = 30):
784 ''' Render a form edit field for the property
785 '''
786 if self._value is None:
787 value = ''
788 else:
789 value = cgi.escape(str(self._value))
790 value = '"'.join(value.split('"'))
791 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
793 def reldate(self, pretty=1):
794 ''' Render the interval between the date and now.
796 If the "pretty" flag is true, then make the display pretty.
797 '''
798 if not self._value:
799 return ''
801 # figure the interval
802 interval = date.Date('.') - self._value
803 if pretty:
804 return interval.pretty()
805 return str(interval)
807 class IntervalHTMLProperty(HTMLProperty):
808 def plain(self):
809 ''' Render a "plain" representation of the property
810 '''
811 if self._value is None:
812 return ''
813 return str(self._value)
815 def pretty(self):
816 ''' Render the interval in a pretty format (eg. "yesterday")
817 '''
818 return self._value.pretty()
820 def field(self, size = 30):
821 ''' Render a form edit field for the property
822 '''
823 if self._value is None:
824 value = ''
825 else:
826 value = cgi.escape(str(self._value))
827 value = '"'.join(value.split('"'))
828 return '<input name="%s" value="%s" size="%s">'%(self._name, value, size)
830 class LinkHTMLProperty(HTMLProperty):
831 ''' Link HTMLProperty
832 Include the above as well as being able to access the class
833 information. Stringifying the object itself results in the value
834 from the item being displayed. Accessing attributes of this object
835 result in the appropriate entry from the class being queried for the
836 property accessed (so item/assignedto/name would look up the user
837 entry identified by the assignedto property on item, and then the
838 name property of that user)
839 '''
840 def __getattr__(self, attr):
841 ''' return a new HTMLItem '''
842 #print 'Link.getattr', (self, attr, self._value)
843 if not self._value:
844 raise AttributeError, "Can't access missing value"
845 if self._prop.classname == 'user':
846 klass = HTMLUser
847 else:
848 klass = HTMLItem
849 i = klass(self._client, self._prop.classname, self._value)
850 return getattr(i, attr)
852 def plain(self, escape=0):
853 ''' Render a "plain" representation of the property
854 '''
855 if self._value is None:
856 return ''
857 linkcl = self._db.classes[self._prop.classname]
858 k = linkcl.labelprop(1)
859 value = str(linkcl.get(self._value, k))
860 if escape:
861 value = cgi.escape(value)
862 return value
864 def field(self, showid=0, size=None):
865 ''' Render a form edit field for the property
866 '''
867 linkcl = self._db.getclass(self._prop.classname)
868 if linkcl.getprops().has_key('order'):
869 sort_on = 'order'
870 else:
871 sort_on = linkcl.labelprop()
872 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
873 # TODO: make this a field display, not a menu one!
874 l = ['<select name="%s">'%self._name]
875 k = linkcl.labelprop(1)
876 if self._value is None:
877 s = 'selected '
878 else:
879 s = ''
880 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
881 for optionid in options:
882 option = linkcl.get(optionid, k)
883 s = ''
884 if optionid == self._value:
885 s = 'selected '
886 if showid:
887 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
888 else:
889 lab = option
890 if size is not None and len(lab) > size:
891 lab = lab[:size-3] + '...'
892 lab = cgi.escape(lab)
893 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
894 l.append('</select>')
895 return '\n'.join(l)
897 def menu(self, size=None, height=None, showid=0, additional=[],
898 **conditions):
899 ''' Render a form select list for this property
900 '''
901 value = self._value
903 # sort function
904 sortfunc = make_sort_function(self._db, self._prop.classname)
906 # force the value to be a single choice
907 if isinstance(value, type('')):
908 value = value[0]
909 linkcl = self._db.getclass(self._prop.classname)
910 l = ['<select name="%s">'%self._name]
911 k = linkcl.labelprop(1)
912 s = ''
913 if value is None:
914 s = 'selected '
915 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
916 if linkcl.getprops().has_key('order'):
917 sort_on = ('+', 'order')
918 else:
919 sort_on = ('+', linkcl.labelprop())
920 options = linkcl.filter(None, conditions, sort_on, (None, None))
921 for optionid in options:
922 option = linkcl.get(optionid, k)
923 s = ''
924 if value in [optionid, option]:
925 s = 'selected '
926 if showid:
927 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
928 else:
929 lab = option
930 if size is not None and len(lab) > size:
931 lab = lab[:size-3] + '...'
932 if additional:
933 m = []
934 for propname in additional:
935 m.append(linkcl.get(optionid, propname))
936 lab = lab + ' (%s)'%', '.join(map(str, m))
937 lab = cgi.escape(lab)
938 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
939 l.append('</select>')
940 return '\n'.join(l)
941 # def checklist(self, ...)
943 class MultilinkHTMLProperty(HTMLProperty):
944 ''' Multilink HTMLProperty
946 Also be iterable, returning a wrapper object like the Link case for
947 each entry in the multilink.
948 '''
949 def __len__(self):
950 ''' length of the multilink '''
951 return len(self._value)
953 def __getattr__(self, attr):
954 ''' no extended attribute accesses make sense here '''
955 raise AttributeError, attr
957 def __getitem__(self, num):
958 ''' iterate and return a new HTMLItem
959 '''
960 #print 'Multi.getitem', (self, num)
961 value = self._value[num]
962 if self._prop.classname == 'user':
963 klass = HTMLUser
964 else:
965 klass = HTMLItem
966 return klass(self._client, self._prop.classname, value)
968 def __contains__(self, value):
969 ''' Support the "in" operator
970 '''
971 return value in self._value
973 def reverse(self):
974 ''' return the list in reverse order
975 '''
976 l = self._value[:]
977 l.reverse()
978 if self._prop.classname == 'user':
979 klass = HTMLUser
980 else:
981 klass = HTMLItem
982 return [klass(self._client, self._prop.classname, value) for value in l]
984 def plain(self, escape=0):
985 ''' Render a "plain" representation of the property
986 '''
987 linkcl = self._db.classes[self._prop.classname]
988 k = linkcl.labelprop(1)
989 labels = []
990 for v in self._value:
991 labels.append(linkcl.get(v, k))
992 value = ', '.join(labels)
993 if escape:
994 value = cgi.escape(value)
995 return value
997 def field(self, size=30, showid=0):
998 ''' Render a form edit field for the property
999 '''
1000 sortfunc = make_sort_function(self._db, self._prop.classname)
1001 linkcl = self._db.getclass(self._prop.classname)
1002 value = self._value[:]
1003 if value:
1004 value.sort(sortfunc)
1005 # map the id to the label property
1006 if not linkcl.getkey():
1007 showid=1
1008 if not showid:
1009 k = linkcl.labelprop(1)
1010 value = [linkcl.get(v, k) for v in value]
1011 value = cgi.escape(','.join(value))
1012 return '<input name="%s" size="%s" value="%s">'%(self._name, size, value)
1014 def menu(self, size=None, height=None, showid=0, additional=[],
1015 **conditions):
1016 ''' Render a form select list for this property
1017 '''
1018 value = self._value
1020 # sort function
1021 sortfunc = make_sort_function(self._db, self._prop.classname)
1023 linkcl = self._db.getclass(self._prop.classname)
1024 if linkcl.getprops().has_key('order'):
1025 sort_on = ('+', 'order')
1026 else:
1027 sort_on = ('+', linkcl.labelprop())
1028 options = linkcl.filter(None, conditions, sort_on, (None,None))
1029 height = height or min(len(options), 7)
1030 l = ['<select multiple name="%s" size="%s">'%(self._name, height)]
1031 k = linkcl.labelprop(1)
1032 for optionid in options:
1033 option = linkcl.get(optionid, k)
1034 s = ''
1035 if optionid in value or option in value:
1036 s = 'selected '
1037 if showid:
1038 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1039 else:
1040 lab = option
1041 if size is not None and len(lab) > size:
1042 lab = lab[:size-3] + '...'
1043 if additional:
1044 m = []
1045 for propname in additional:
1046 m.append(linkcl.get(optionid, propname))
1047 lab = lab + ' (%s)'%', '.join(m)
1048 lab = cgi.escape(lab)
1049 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1050 lab))
1051 l.append('</select>')
1052 return '\n'.join(l)
1054 # set the propclasses for HTMLItem
1055 propclasses = (
1056 (hyperdb.String, StringHTMLProperty),
1057 (hyperdb.Number, NumberHTMLProperty),
1058 (hyperdb.Boolean, BooleanHTMLProperty),
1059 (hyperdb.Date, DateHTMLProperty),
1060 (hyperdb.Interval, IntervalHTMLProperty),
1061 (hyperdb.Password, PasswordHTMLProperty),
1062 (hyperdb.Link, LinkHTMLProperty),
1063 (hyperdb.Multilink, MultilinkHTMLProperty),
1064 )
1066 def make_sort_function(db, classname):
1067 '''Make a sort function for a given class
1068 '''
1069 linkcl = db.getclass(classname)
1070 if linkcl.getprops().has_key('order'):
1071 sort_on = 'order'
1072 else:
1073 sort_on = linkcl.labelprop()
1074 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1075 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1076 return sortfunc
1078 def handleListCGIValue(value):
1079 ''' Value is either a single item or a list of items. Each item has a
1080 .value that we're actually interested in.
1081 '''
1082 if isinstance(value, type([])):
1083 return [value.value for value in value]
1084 else:
1085 value = value.value.strip()
1086 if not value:
1087 return []
1088 return value.split(',')
1090 class ShowDict:
1091 ''' A convenience access to the :columns index parameters
1092 '''
1093 def __init__(self, columns):
1094 self.columns = {}
1095 for col in columns:
1096 self.columns[col] = 1
1097 def __getitem__(self, name):
1098 return self.columns.has_key(name)
1100 class HTMLRequest:
1101 ''' The *request*, holding the CGI form and environment.
1103 "form" the CGI form as a cgi.FieldStorage
1104 "env" the CGI environment variables
1105 "url" the current URL path for this request
1106 "base" the base URL for this instance
1107 "user" a HTMLUser instance for this user
1108 "classname" the current classname (possibly None)
1109 "template" the current template (suffix, also possibly None)
1111 Index args:
1112 "columns" dictionary of the columns to display in an index page
1113 "show" a convenience access to columns - request/show/colname will
1114 be true if the columns should be displayed, false otherwise
1115 "sort" index sort column (direction, column name)
1116 "group" index grouping property (direction, column name)
1117 "filter" properties to filter the index on
1118 "filterspec" values to filter the index on
1119 "search_text" text to perform a full-text search on for an index
1121 '''
1122 def __init__(self, client):
1123 self.client = client
1125 # easier access vars
1126 self.form = client.form
1127 self.env = client.env
1128 self.base = client.base
1129 self.url = client.url
1130 self.user = HTMLUser(client, 'user', client.userid)
1132 # store the current class name and action
1133 self.classname = client.classname
1134 self.template = client.template
1136 self._post_init()
1138 def _post_init(self):
1139 ''' Set attributes based on self.form
1140 '''
1141 # extract the index display information from the form
1142 self.columns = []
1143 if self.form.has_key(':columns'):
1144 self.columns = handleListCGIValue(self.form[':columns'])
1145 self.show = ShowDict(self.columns)
1147 # sorting
1148 self.sort = (None, None)
1149 if self.form.has_key(':sort'):
1150 sort = self.form[':sort'].value
1151 if sort.startswith('-'):
1152 self.sort = ('-', sort[1:])
1153 else:
1154 self.sort = ('+', sort)
1155 if self.form.has_key(':sortdir'):
1156 self.sort = ('-', self.sort[1])
1158 # grouping
1159 self.group = (None, None)
1160 if self.form.has_key(':group'):
1161 group = self.form[':group'].value
1162 if group.startswith('-'):
1163 self.group = ('-', group[1:])
1164 else:
1165 self.group = ('+', group)
1166 if self.form.has_key(':groupdir'):
1167 self.group = ('-', self.group[1])
1169 # filtering
1170 self.filter = []
1171 if self.form.has_key(':filter'):
1172 self.filter = handleListCGIValue(self.form[':filter'])
1173 self.filterspec = {}
1174 if self.classname is not None:
1175 props = self.client.db.getclass(self.classname).getprops()
1176 for name in self.filter:
1177 if self.form.has_key(name):
1178 prop = props[name]
1179 fv = self.form[name]
1180 if (isinstance(prop, hyperdb.Link) or
1181 isinstance(prop, hyperdb.Multilink)):
1182 self.filterspec[name] = handleListCGIValue(fv)
1183 else:
1184 self.filterspec[name] = fv.value
1186 # full-text search argument
1187 self.search_text = None
1188 if self.form.has_key(':search_text'):
1189 self.search_text = self.form[':search_text'].value
1191 # pagination - size and start index
1192 # figure batch args
1193 if self.form.has_key(':pagesize'):
1194 self.pagesize = int(self.form[':pagesize'].value)
1195 else:
1196 self.pagesize = 50
1197 if self.form.has_key(':startwith'):
1198 self.startwith = int(self.form[':startwith'].value)
1199 else:
1200 self.startwith = 0
1202 def updateFromURL(self, url):
1203 ''' Parse the URL for query args, and update my attributes using the
1204 values.
1205 '''
1206 self.form = {}
1207 for name, value in cgi.parse_qsl(url):
1208 if self.form.has_key(name):
1209 if isinstance(self.form[name], type([])):
1210 self.form[name].append(cgi.MiniFieldStorage(name, value))
1211 else:
1212 self.form[name] = [self.form[name],
1213 cgi.MiniFieldStorage(name, value)]
1214 else:
1215 self.form[name] = cgi.MiniFieldStorage(name, value)
1216 self._post_init()
1218 def update(self, kwargs):
1219 ''' Update my attributes using the keyword args
1220 '''
1221 self.__dict__.update(kwargs)
1222 if kwargs.has_key('columns'):
1223 self.show = ShowDict(self.columns)
1225 def description(self):
1226 ''' Return a description of the request - handle for the page title.
1227 '''
1228 s = [self.client.db.config.TRACKER_NAME]
1229 if self.classname:
1230 if self.client.nodeid:
1231 s.append('- %s%s'%(self.classname, self.client.nodeid))
1232 else:
1233 if self.template == 'item':
1234 s.append('- new %s'%self.classname)
1235 elif self.template == 'index':
1236 s.append('- %s index'%self.classname)
1237 else:
1238 s.append('- %s %s'%(self.classname, self.template))
1239 else:
1240 s.append('- home')
1241 return ' '.join(s)
1243 def __str__(self):
1244 d = {}
1245 d.update(self.__dict__)
1246 f = ''
1247 for k in self.form.keys():
1248 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1249 d['form'] = f
1250 e = ''
1251 for k,v in self.env.items():
1252 e += '\n %r=%r'%(k, v)
1253 d['env'] = e
1254 return '''
1255 form: %(form)s
1256 url: %(url)r
1257 base: %(base)r
1258 classname: %(classname)r
1259 template: %(template)r
1260 columns: %(columns)r
1261 sort: %(sort)r
1262 group: %(group)r
1263 filter: %(filter)r
1264 search_text: %(search_text)r
1265 pagesize: %(pagesize)r
1266 startwith: %(startwith)r
1267 env: %(env)s
1268 '''%d
1270 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1271 filterspec=1):
1272 ''' return the current index args as form elements '''
1273 l = []
1274 s = '<input type="hidden" name="%s" value="%s">'
1275 if columns and self.columns:
1276 l.append(s%(':columns', ','.join(self.columns)))
1277 if sort and self.sort[1] is not None:
1278 if self.sort[0] == '-':
1279 val = '-'+self.sort[1]
1280 else:
1281 val = self.sort[1]
1282 l.append(s%(':sort', val))
1283 if group and self.group[1] is not None:
1284 if self.group[0] == '-':
1285 val = '-'+self.group[1]
1286 else:
1287 val = self.group[1]
1288 l.append(s%(':group', val))
1289 if filter and self.filter:
1290 l.append(s%(':filter', ','.join(self.filter)))
1291 if filterspec:
1292 for k,v in self.filterspec.items():
1293 l.append(s%(k, ','.join(v)))
1294 if self.search_text:
1295 l.append(s%(':search_text', self.search_text))
1296 l.append(s%(':pagesize', self.pagesize))
1297 l.append(s%(':startwith', self.startwith))
1298 return '\n'.join(l)
1300 def indexargs_url(self, url, args):
1301 ''' embed the current index args in a URL '''
1302 l = ['%s=%s'%(k,v) for k,v in args.items()]
1303 if self.columns and not args.has_key(':columns'):
1304 l.append(':columns=%s'%(','.join(self.columns)))
1305 if self.sort[1] is not None and not args.has_key(':sort'):
1306 if self.sort[0] == '-':
1307 val = '-'+self.sort[1]
1308 else:
1309 val = self.sort[1]
1310 l.append(':sort=%s'%val)
1311 if self.group[1] is not None and not args.has_key(':group'):
1312 if self.group[0] == '-':
1313 val = '-'+self.group[1]
1314 else:
1315 val = self.group[1]
1316 l.append(':group=%s'%val)
1317 if self.filter and not args.has_key(':columns'):
1318 l.append(':filter=%s'%(','.join(self.filter)))
1319 for k,v in self.filterspec.items():
1320 if not args.has_key(k):
1321 l.append('%s=%s'%(k, ','.join(v)))
1322 if self.search_text and not args.has_key(':search_text'):
1323 l.append(':search_text=%s'%self.search_text)
1324 if not args.has_key(':pagesize'):
1325 l.append(':pagesize=%s'%self.pagesize)
1326 if not args.has_key(':startwith'):
1327 l.append(':startwith=%s'%self.startwith)
1328 return '%s?%s'%(url, '&'.join(l))
1329 indexargs_href = indexargs_url
1331 def base_javascript(self):
1332 return '''
1333 <script language="javascript">
1334 submitted = false;
1335 function submit_once() {
1336 if (submitted) {
1337 alert("Your request is being processed.\\nPlease be patient.");
1338 return 0;
1339 }
1340 submitted = true;
1341 return 1;
1342 }
1344 function help_window(helpurl, width, height) {
1345 HelpWin = window.open('%s/' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1346 }
1347 </script>
1348 '''%self.base
1350 def batch(self):
1351 ''' Return a batch object for results from the "current search"
1352 '''
1353 filterspec = self.filterspec
1354 sort = self.sort
1355 group = self.group
1357 # get the list of ids we're batching over
1358 klass = self.client.db.getclass(self.classname)
1359 if self.search_text:
1360 matches = self.client.db.indexer.search(
1361 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1362 else:
1363 matches = None
1364 l = klass.filter(matches, filterspec, sort, group)
1366 # map the item ids to instances
1367 if self.classname == 'user':
1368 klass = HTMLUser
1369 else:
1370 klass = HTMLItem
1371 l = [klass(self.client, self.classname, item) for item in l]
1373 # return the batch object
1374 return Batch(self.client, l, self.pagesize, self.startwith)
1376 # extend the standard ZTUtils Batch object to remove dependency on
1377 # Acquisition and add a couple of useful methods
1378 class Batch(ZTUtils.Batch):
1379 ''' Use me to turn a list of items, or item ids of a given class, into a
1380 series of batches.
1382 ========= ========================================================
1383 Parameter Usage
1384 ========= ========================================================
1385 sequence a list of HTMLItems
1386 size how big to make the sequence.
1387 start where to start (0-indexed) in the sequence.
1388 end where to end (0-indexed) in the sequence.
1389 orphan if the next batch would contain less items than this
1390 value, then it is combined with this batch
1391 overlap the number of items shared between adjacent batches
1392 ========= ========================================================
1394 Attributes: Note that the "start" attribute, unlike the
1395 argument, is a 1-based index (I know, lame). "first" is the
1396 0-based index. "length" is the actual number of elements in
1397 the batch.
1399 "sequence_length" is the length of the original, unbatched, sequence.
1400 '''
1401 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1402 overlap=0):
1403 self.client = client
1404 self.last_index = self.last_item = None
1405 self.current_item = None
1406 self.sequence_length = len(sequence)
1407 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1408 overlap)
1410 # overwrite so we can late-instantiate the HTMLItem instance
1411 def __getitem__(self, index):
1412 if index < 0:
1413 if index + self.end < self.first: raise IndexError, index
1414 return self._sequence[index + self.end]
1416 if index >= self.length:
1417 raise IndexError, index
1419 # move the last_item along - but only if the fetched index changes
1420 # (for some reason, index 0 is fetched twice)
1421 if index != self.last_index:
1422 self.last_item = self.current_item
1423 self.last_index = index
1425 self.current_item = self._sequence[index + self.first]
1426 return self.current_item
1428 def propchanged(self, property):
1429 ''' Detect if the property marked as being the group property
1430 changed in the last iteration fetch
1431 '''
1432 if (self.last_item is None or
1433 self.last_item[property] != self.current_item[property]):
1434 return 1
1435 return 0
1437 # override these 'cos we don't have access to acquisition
1438 def previous(self):
1439 if self.start == 1:
1440 return None
1441 return Batch(self.client, self._sequence, self._size,
1442 self.first - self._size + self.overlap, 0, self.orphan,
1443 self.overlap)
1445 def next(self):
1446 try:
1447 self._sequence[self.end]
1448 except IndexError:
1449 return None
1450 return Batch(self.client, self._sequence, self._size,
1451 self.end - self.overlap, 0, self.orphan, self.overlap)
1453 class TemplatingUtils:
1454 ''' Utilities for templating
1455 '''
1456 def __init__(self, client):
1457 self.client = client
1458 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1459 return Batch(self.client, sequence, size, start, end, orphan,
1460 overlap)