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