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