372b8fea2a63eb9d0023a55af6041ea8364a536b
1 """Implements the API used in the HTML templating for the web interface.
2 """
3 __docformat__ = 'restructuredtext'
5 from __future__ import nested_scopes
7 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
9 from roundup import hyperdb, date, rcsv
10 from roundup.i18n import _
12 try:
13 import cPickle as pickle
14 except ImportError:
15 import pickle
16 try:
17 import cStringIO as StringIO
18 except ImportError:
19 import StringIO
20 try:
21 import StructuredText
22 except ImportError:
23 StructuredText = None
25 # bring in the templating support
26 from roundup.cgi.PageTemplates import PageTemplate
27 from roundup.cgi.PageTemplates.Expressions import getEngine
28 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
29 from roundup.cgi import ZTUtils
31 class NoTemplate(Exception):
32 pass
34 class Unauthorised(Exception):
35 def __init__(self, action, klass):
36 self.action = action
37 self.klass = klass
38 def __str__(self):
39 return 'You are not allowed to %s items of class %s'%(self.action,
40 self.klass)
42 def find_template(dir, name, extension):
43 ''' Find a template in the nominated dir
44 '''
45 # find the source
46 if extension:
47 filename = '%s.%s'%(name, extension)
48 else:
49 filename = name
51 # try old-style
52 src = os.path.join(dir, filename)
53 if os.path.exists(src):
54 return (src, filename)
56 # try with a .html extension (new-style)
57 filename = filename + '.html'
58 src = os.path.join(dir, filename)
59 if os.path.exists(src):
60 return (src, filename)
62 # no extension == no generic template is possible
63 if not extension:
64 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
66 # try for a _generic template
67 generic = '_generic.%s'%extension
68 src = os.path.join(dir, generic)
69 if os.path.exists(src):
70 return (src, generic)
72 # finally, try _generic.html
73 generic = generic + '.html'
74 src = os.path.join(dir, generic)
75 if os.path.exists(src):
76 return (src, generic)
78 raise NoTemplate, 'No template file exists for templating "%s" '\
79 'with template "%s" (neither "%s" nor "%s")'%(name, extension,
80 filename, generic)
82 class Templates:
83 templates = {}
85 def __init__(self, dir):
86 self.dir = dir
88 def precompileTemplates(self):
89 ''' Go through a directory and precompile all the templates therein
90 '''
91 for filename in os.listdir(self.dir):
92 if os.path.isdir(filename): continue
93 if '.' in filename:
94 name, extension = filename.split('.')
95 self.get(name, extension)
96 else:
97 self.get(filename, None)
99 def get(self, name, extension=None):
100 ''' Interface to get a template, possibly loading a compiled template.
102 "name" and "extension" indicate the template we're after, which in
103 most cases will be "name.extension". If "extension" is None, then
104 we look for a template just called "name" with no extension.
106 If the file "name.extension" doesn't exist, we look for
107 "_generic.extension" as a fallback.
108 '''
109 # default the name to "home"
110 if name is None:
111 name = 'home'
112 elif extension is None and '.' in name:
113 # split name
114 name, extension = name.split('.')
116 # find the source
117 src, filename = find_template(self.dir, name, extension)
119 # has it changed?
120 try:
121 stime = os.stat(src)[os.path.stat.ST_MTIME]
122 except os.error, error:
123 if error.errno != errno.ENOENT:
124 raise
126 if self.templates.has_key(src) and \
127 stime <= self.templates[src].mtime:
128 # compiled template is up to date
129 return self.templates[src]
131 # compile the template
132 self.templates[src] = pt = RoundupPageTemplate()
133 # use pt_edit so we can pass the content_type guess too
134 content_type = mimetypes.guess_type(filename)[0] or 'text/html'
135 pt.pt_edit(open(src).read(), content_type)
136 pt.id = filename
137 pt.mtime = stime
138 return pt
140 def __getitem__(self, name):
141 name, extension = os.path.splitext(name)
142 if extension:
143 extension = extension[1:]
144 try:
145 return self.get(name, extension)
146 except NoTemplate, message:
147 raise KeyError, message
149 class RoundupPageTemplate(PageTemplate.PageTemplate):
150 '''A Roundup-specific PageTemplate.
152 Interrogate the client to set up the various template variables to
153 be available:
155 *context*
156 this is one of three things:
158 1. None - we're viewing a "home" page
159 2. The current class of item being displayed. This is an HTMLClass
160 instance.
161 3. The current item from the database, if we're viewing a specific
162 item, as an HTMLItem instance.
163 *request*
164 Includes information about the current request, including:
166 - the url
167 - the current index information (``filterspec``, ``filter`` args,
168 ``properties``, etc) parsed out of the form.
169 - methods for easy filterspec link generation
170 - *user*, the current user node as an HTMLItem instance
171 - *form*, the current CGI form information as a FieldStorage
172 *config*
173 The current tracker config.
174 *db*
175 The current database, used to access arbitrary database items.
176 *utils*
177 This is a special class that has its base in the TemplatingUtils
178 class in this file. If the tracker interfaces module defines a
179 TemplatingUtils class then it is mixed in, overriding the methods
180 in the base class.
181 '''
182 def getContext(self, client, classname, request):
183 # construct the TemplatingUtils class
184 utils = TemplatingUtils
185 if hasattr(client.instance.interfaces, 'TemplatingUtils'):
186 class utils(client.instance.interfaces.TemplatingUtils, utils):
187 pass
189 c = {
190 'options': {},
191 'nothing': None,
192 'request': request,
193 'db': HTMLDatabase(client),
194 'config': client.instance.config,
195 'tracker': client.instance,
196 'utils': utils(client),
197 'templates': Templates(client.instance.config.TEMPLATES),
198 'template': self,
199 }
200 # add in the item if there is one
201 if client.nodeid:
202 if classname == 'user':
203 c['context'] = HTMLUser(client, classname, client.nodeid,
204 anonymous=1)
205 else:
206 c['context'] = HTMLItem(client, classname, client.nodeid,
207 anonymous=1)
208 elif client.db.classes.has_key(classname):
209 if classname == 'user':
210 c['context'] = HTMLUserClass(client, classname, anonymous=1)
211 else:
212 c['context'] = HTMLClass(client, classname, anonymous=1)
213 return c
215 def render(self, client, classname, request, **options):
216 """Render this Page Template"""
218 if not self._v_cooked:
219 self._cook()
221 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
223 if self._v_errors:
224 raise PageTemplate.PTRuntimeError, \
225 'Page Template %s has errors.'%self.id
227 # figure the context
228 classname = classname or client.classname
229 request = request or HTMLRequest(client)
230 c = self.getContext(client, classname, request)
231 c.update({'options': options})
233 # and go
234 output = StringIO.StringIO()
235 TALInterpreter(self._v_program, self.macros,
236 getEngine().getContext(c), output, tal=1, strictinsert=0)()
237 return output.getvalue()
239 def __repr__(self):
240 return '<Roundup PageTemplate %r>'%self.id
242 class HTMLDatabase:
243 ''' Return HTMLClasses for valid class fetches
244 '''
245 def __init__(self, client):
246 self._client = client
247 self._db = client.db
249 # we want config to be exposed
250 self.config = client.db.config
252 def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
253 # check to see if we're actually accessing an item
254 m = desre.match(item)
255 if m:
256 cl = m.group('cl')
257 self._client.db.getclass(cl)
258 if cl == 'user':
259 klass = HTMLUser
260 else:
261 klass = HTMLItem
262 return klass(self._client, cl, m.group('id'))
263 else:
264 self._client.db.getclass(item)
265 if item == 'user':
266 return HTMLUserClass(self._client, item)
267 return HTMLClass(self._client, item)
269 def __getattr__(self, attr):
270 try:
271 return self[attr]
272 except KeyError:
273 raise AttributeError, attr
275 def classes(self):
276 l = self._client.db.classes.keys()
277 l.sort()
278 m = []
279 for item in l:
280 if item == 'user':
281 m.append(HTMLUserClass(self._client, item))
282 m.append(HTMLClass(self._client, item))
283 return m
285 def lookupIds(db, prop, ids, fail_ok=0, num_re=re.compile('-?\d+')):
286 ''' "fail_ok" should be specified if we wish to pass through bad values
287 (most likely form values that we wish to represent back to the user)
288 '''
289 cl = db.getclass(prop.classname)
290 l = []
291 for entry in ids:
292 if num_re.match(entry):
293 l.append(entry)
294 else:
295 try:
296 l.append(cl.lookup(entry))
297 except (TypeError, KeyError):
298 if fail_ok:
299 # pass through the bad value
300 l.append(entry)
301 return l
303 def lookupKeys(linkcl, key, ids, num_re=re.compile('-?\d+')):
304 ''' Look up the "key" values for "ids" list - though some may already
305 be key values, not ids.
306 '''
307 l = []
308 for entry in ids:
309 if num_re.match(entry):
310 l.append(linkcl.get(entry, key))
311 else:
312 l.append(entry)
313 return l
315 class HTMLPermissions:
316 ''' Helpers that provide answers to commonly asked Permission questions.
317 '''
318 def is_edit_ok(self):
319 ''' Is the user allowed to Edit the current class?
320 '''
321 return self._db.security.hasPermission('Edit', self._client.userid,
322 self._classname)
324 def is_view_ok(self):
325 ''' Is the user allowed to View the current class?
326 '''
327 return self._db.security.hasPermission('View', self._client.userid,
328 self._classname)
330 def is_only_view_ok(self):
331 ''' Is the user only allowed to View (ie. not Edit) the current class?
332 '''
333 return self.is_view_ok() and not self.is_edit_ok()
335 def view_check(self):
336 ''' Raise the Unauthorised exception if the user's not permitted to
337 view this class.
338 '''
339 if not self.is_view_ok():
340 raise Unauthorised("view", self._classname)
342 def edit_check(self):
343 ''' Raise the Unauthorised exception if the user's not permitted to
344 edit this class.
345 '''
346 if not self.is_edit_ok():
347 raise Unauthorised("edit", self._classname)
349 def input_html4(**attrs):
350 """Generate an 'input' (html4) element with given attributes"""
351 return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
353 def input_xhtml(**attrs):
354 """Generate an 'input' (xhtml) element with given attributes"""
355 return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
357 class HTMLInputMixin:
358 ''' requires a _client property '''
359 def __init__(self):
360 html_version = 'html4'
361 if hasattr(self._client.instance.config, 'HTML_VERSION'):
362 html_version = self._client.instance.config.HTML_VERSION
363 if html_version == 'xhtml':
364 self.input = input_xhtml
365 else:
366 self.input = input_html4
368 class HTMLClass(HTMLInputMixin, HTMLPermissions):
369 ''' Accesses through a class (either through *class* or *db.<classname>*)
370 '''
371 def __init__(self, client, classname, anonymous=0):
372 self._client = client
373 self._db = client.db
374 self._anonymous = anonymous
376 # we want classname to be exposed, but _classname gives a
377 # consistent API for extending Class/Item
378 self._classname = self.classname = classname
379 self._klass = self._db.getclass(self.classname)
380 self._props = self._klass.getprops()
382 HTMLInputMixin.__init__(self)
384 def __repr__(self):
385 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
387 def __getitem__(self, item):
388 ''' return an HTMLProperty instance
389 '''
390 #print 'HTMLClass.getitem', (self, item)
392 # we don't exist
393 if item == 'id':
394 return None
396 # get the property
397 try:
398 prop = self._props[item]
399 except KeyError:
400 raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
402 # look up the correct HTMLProperty class
403 form = self._client.form
404 for klass, htmlklass in propclasses:
405 if not isinstance(prop, klass):
406 continue
407 if form.has_key(item):
408 if isinstance(prop, hyperdb.Multilink):
409 value = lookupIds(self._db, prop,
410 handleListCGIValue(form[item]), fail_ok=1)
411 elif isinstance(prop, hyperdb.Link):
412 value = form[item].value.strip()
413 if value:
414 value = lookupIds(self._db, prop, [value],
415 fail_ok=1)[0]
416 else:
417 value = None
418 else:
419 value = form[item].value.strip() or None
420 else:
421 if isinstance(prop, hyperdb.Multilink):
422 value = []
423 else:
424 value = None
425 return htmlklass(self._client, self._classname, '', prop, item,
426 value, self._anonymous)
428 # no good
429 raise KeyError, item
431 def __getattr__(self, attr):
432 ''' convenience access '''
433 try:
434 return self[attr]
435 except KeyError:
436 raise AttributeError, attr
438 def designator(self):
439 ''' Return this class' designator (classname) '''
440 return self._classname
442 def getItem(self, itemid, num_re=re.compile('-?\d+')):
443 ''' Get an item of this class by its item id.
444 '''
445 # make sure we're looking at an itemid
446 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
447 itemid = self._klass.lookup(itemid)
449 if self.classname == 'user':
450 klass = HTMLUser
451 else:
452 klass = HTMLItem
454 return klass(self._client, self.classname, itemid)
456 def properties(self, sort=1):
457 ''' Return HTMLProperty for all of this class' properties.
458 '''
459 l = []
460 for name, prop in self._props.items():
461 for klass, htmlklass in propclasses:
462 if isinstance(prop, hyperdb.Multilink):
463 value = []
464 else:
465 value = None
466 if isinstance(prop, klass):
467 l.append(htmlklass(self._client, self._classname, '',
468 prop, name, value, self._anonymous))
469 if sort:
470 l.sort(lambda a,b:cmp(a._name, b._name))
471 return l
473 def list(self, sort_on=None):
474 ''' List all items in this class.
475 '''
476 if self.classname == 'user':
477 klass = HTMLUser
478 else:
479 klass = HTMLItem
481 # get the list and sort it nicely
482 l = self._klass.list()
483 sortfunc = make_sort_function(self._db, self.classname, sort_on)
484 l.sort(sortfunc)
486 l = [klass(self._client, self.classname, x) for x in l]
487 return l
489 def csv(self):
490 ''' Return the items of this class as a chunk of CSV text.
491 '''
492 if rcsv.error:
493 return rcsv.error
495 props = self.propnames()
496 s = StringIO.StringIO()
497 writer = rcsv.writer(s, rcsv.comma_separated)
498 writer.writerow(props)
499 for nodeid in self._klass.list():
500 l = []
501 for name in props:
502 value = self._klass.get(nodeid, name)
503 if value is None:
504 l.append('')
505 elif isinstance(value, type([])):
506 l.append(':'.join(map(str, value)))
507 else:
508 l.append(str(self._klass.get(nodeid, name)))
509 writer.writerow(l)
510 return s.getvalue()
512 def propnames(self):
513 ''' Return the list of the names of the properties of this class.
514 '''
515 idlessprops = self._klass.getprops(protected=0).keys()
516 idlessprops.sort()
517 return ['id'] + idlessprops
519 def filter(self, request=None, filterspec={}, sort=(None,None),
520 group=(None,None)):
521 ''' Return a list of items from this class, filtered and sorted
522 by the current requested filterspec/filter/sort/group args
524 "request" takes precedence over the other three arguments.
525 '''
526 if request is not None:
527 filterspec = request.filterspec
528 sort = request.sort
529 group = request.group
530 if self.classname == 'user':
531 klass = HTMLUser
532 else:
533 klass = HTMLItem
534 l = [klass(self._client, self.classname, x)
535 for x in self._klass.filter(None, filterspec, sort, group)]
536 return l
538 def classhelp(self, properties=None, label='(list)', width='500',
539 height='400', property=''):
540 ''' Pop up a javascript window with class help
542 This generates a link to a popup window which displays the
543 properties indicated by "properties" of the class named by
544 "classname". The "properties" should be a comma-separated list
545 (eg. 'id,name,description'). Properties defaults to all the
546 properties of a class (excluding id, creator, created and
547 activity).
549 You may optionally override the label displayed, the width and
550 height. The popup window will be resizable and scrollable.
552 If the "property" arg is given, it's passed through to the
553 javascript help_window function.
554 '''
555 if properties is None:
556 properties = self._klass.getprops(protected=0).keys()
557 properties.sort()
558 properties = ','.join(properties)
559 if property:
560 property = '&property=%s'%property
561 return '<a class="classhelp" href="javascript:help_window(\'%s?'\
562 '@startwith=0&@template=help&properties=%s%s\', \'%s\', \
563 \'%s\')">%s</a>'%(self.classname, properties, property, width,
564 height, label)
566 def submit(self, label="Submit New Entry"):
567 ''' Generate a submit button (and action hidden element)
568 '''
569 self.view_check()
570 if self.is_edit_ok():
571 return self.input(type="hidden",name="@action",value="new") + \
572 '\n' + self.input(type="submit",name="submit",value=label)
573 return ''
575 def history(self):
576 self.view_check()
577 return 'New node - no history'
579 def renderWith(self, name, **kwargs):
580 ''' Render this class with the given template.
581 '''
582 # create a new request and override the specified args
583 req = HTMLRequest(self._client)
584 req.classname = self.classname
585 req.update(kwargs)
587 # new template, using the specified classname and request
588 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
590 # use our fabricated request
591 args = {
592 'ok_message': self._client.ok_message,
593 'error_message': self._client.error_message
594 }
595 return pt.render(self._client, self.classname, req, **args)
597 class HTMLItem(HTMLInputMixin, HTMLPermissions):
598 ''' Accesses through an *item*
599 '''
600 def __init__(self, client, classname, nodeid, anonymous=0):
601 self._client = client
602 self._db = client.db
603 self._classname = classname
604 self._nodeid = nodeid
605 self._klass = self._db.getclass(classname)
606 self._props = self._klass.getprops()
608 # do we prefix the form items with the item's identification?
609 self._anonymous = anonymous
611 HTMLInputMixin.__init__(self)
613 def __repr__(self):
614 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
615 self._nodeid)
617 def __getitem__(self, item):
618 ''' return an HTMLProperty instance
619 '''
620 #print 'HTMLItem.getitem', (self, item)
621 if item == 'id':
622 return self._nodeid
624 # get the property
625 prop = self._props[item]
627 # get the value, handling missing values
628 value = None
629 if int(self._nodeid) > 0:
630 value = self._klass.get(self._nodeid, item, None)
631 if value is None:
632 if isinstance(self._props[item], hyperdb.Multilink):
633 value = []
635 # look up the correct HTMLProperty class
636 for klass, htmlklass in propclasses:
637 if isinstance(prop, klass):
638 return htmlklass(self._client, self._classname,
639 self._nodeid, prop, item, value, self._anonymous)
641 raise KeyError, item
643 def __getattr__(self, attr):
644 ''' convenience access to properties '''
645 try:
646 return self[attr]
647 except KeyError:
648 raise AttributeError, attr
650 def designator(self):
651 """Return this item's designator (classname + id)."""
652 return '%s%s'%(self._classname, self._nodeid)
654 def is_retired(self):
655 """Is this item retired?"""
656 return self._klass.is_retired(self._nodeid)
658 def submit(self, label="Submit Changes"):
659 """Generate a submit button.
661 Also sneak in the lastactivity and action hidden elements.
662 """
663 return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
664 self.input(type="hidden", name="@action", value="edit") + '\n' + \
665 self.input(type="submit", name="submit", value=label)
667 def journal(self, direction='descending'):
668 ''' Return a list of HTMLJournalEntry instances.
669 '''
670 # XXX do this
671 return []
673 def history(self, direction='descending', dre=re.compile('\d+')):
674 self.view_check()
676 l = ['<table class="history">'
677 '<tr><th colspan="4" class="header">',
678 _('History'),
679 '</th></tr><tr>',
680 _('<th>Date</th>'),
681 _('<th>User</th>'),
682 _('<th>Action</th>'),
683 _('<th>Args</th>'),
684 '</tr>']
685 current = {}
686 comments = {}
687 history = self._klass.history(self._nodeid)
688 history.sort()
689 timezone = self._db.getUserTimezone()
690 if direction == 'descending':
691 history.reverse()
692 for prop_n in self._props.keys():
693 prop = self[prop_n]
694 if isinstance(prop, HTMLProperty):
695 current[prop_n] = prop.plain()
696 # make link if hrefable
697 if (self._props.has_key(prop_n) and
698 isinstance(self._props[prop_n], hyperdb.Link)):
699 classname = self._props[prop_n].classname
700 try:
701 template = find_template(self._db.config.TEMPLATES,
702 classname, 'item')
703 if template[1].startswith('_generic'):
704 raise NoTemplate, 'not really...'
705 except NoTemplate:
706 pass
707 else:
708 id = self._klass.get(self._nodeid, prop_n, None)
709 current[prop_n] = '<a href="%s%s">%s</a>'%(
710 classname, id, current[prop_n])
712 for id, evt_date, user, action, args in history:
713 date_s = str(evt_date.local(timezone)).replace("."," ")
714 arg_s = ''
715 if action == 'link' and type(args) == type(()):
716 if len(args) == 3:
717 linkcl, linkid, key = args
718 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
719 linkcl, linkid, key)
720 else:
721 arg_s = str(args)
723 elif action == 'unlink' and type(args) == type(()):
724 if len(args) == 3:
725 linkcl, linkid, key = args
726 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
727 linkcl, linkid, key)
728 else:
729 arg_s = str(args)
731 elif type(args) == type({}):
732 cell = []
733 for k in args.keys():
734 # try to get the relevant property and treat it
735 # specially
736 try:
737 prop = self._props[k]
738 except KeyError:
739 prop = None
740 if prop is None:
741 # property no longer exists
742 comments['no_exist'] = _('''<em>The indicated property
743 no longer exists</em>''')
744 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
745 continue
747 if args[k] and (isinstance(prop, hyperdb.Multilink) or
748 isinstance(prop, hyperdb.Link)):
749 # figure what the link class is
750 classname = prop.classname
751 try:
752 linkcl = self._db.getclass(classname)
753 except KeyError:
754 labelprop = None
755 comments[classname] = _('''The linked class
756 %(classname)s no longer exists''')%locals()
757 labelprop = linkcl.labelprop(1)
758 try:
759 template = find_template(self._db.config.TEMPLATES,
760 classname, 'item')
761 if template[1].startswith('_generic'):
762 raise NoTemplate, 'not really...'
763 hrefable = 1
764 except NoTemplate:
765 hrefable = 0
767 if isinstance(prop, hyperdb.Multilink) and args[k]:
768 ml = []
769 for linkid in args[k]:
770 if isinstance(linkid, type(())):
771 sublabel = linkid[0] + ' '
772 linkids = linkid[1]
773 else:
774 sublabel = ''
775 linkids = [linkid]
776 subml = []
777 for linkid in linkids:
778 label = classname + linkid
779 # if we have a label property, try to use it
780 # TODO: test for node existence even when
781 # there's no labelprop!
782 try:
783 if labelprop is not None and \
784 labelprop != 'id':
785 label = linkcl.get(linkid, labelprop)
786 except IndexError:
787 comments['no_link'] = _('''<strike>The
788 linked node no longer
789 exists</strike>''')
790 subml.append('<strike>%s</strike>'%label)
791 else:
792 if hrefable:
793 subml.append('<a href="%s%s">%s</a>'%(
794 classname, linkid, label))
795 else:
796 subml.append(label)
797 ml.append(sublabel + ', '.join(subml))
798 cell.append('%s:\n %s'%(k, ', '.join(ml)))
799 elif isinstance(prop, hyperdb.Link) and args[k]:
800 label = classname + args[k]
801 # if we have a label property, try to use it
802 # TODO: test for node existence even when
803 # there's no labelprop!
804 if labelprop is not None and labelprop != 'id':
805 try:
806 label = linkcl.get(args[k], labelprop)
807 except IndexError:
808 comments['no_link'] = _('''<strike>The
809 linked node no longer
810 exists</strike>''')
811 cell.append(' <strike>%s</strike>,\n'%label)
812 # "flag" this is done .... euwww
813 label = None
814 if label is not None:
815 if hrefable:
816 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
817 else:
818 old = label;
819 cell.append('%s: %s' % (k,old))
820 if current.has_key(k):
821 cell[-1] += ' -> %s'%current[k]
822 current[k] = old
824 elif isinstance(prop, hyperdb.Date) and args[k]:
825 d = date.Date(args[k]).local(timezone)
826 cell.append('%s: %s'%(k, str(d)))
827 if current.has_key(k):
828 cell[-1] += ' -> %s' % current[k]
829 current[k] = str(d)
831 elif isinstance(prop, hyperdb.Interval) and args[k]:
832 d = date.Interval(args[k])
833 cell.append('%s: %s'%(k, str(d)))
834 if current.has_key(k):
835 cell[-1] += ' -> %s'%current[k]
836 current[k] = str(d)
838 elif isinstance(prop, hyperdb.String) and args[k]:
839 cell.append('%s: %s'%(k, cgi.escape(args[k])))
840 if current.has_key(k):
841 cell[-1] += ' -> %s'%current[k]
842 current[k] = cgi.escape(args[k])
844 elif not args[k]:
845 if current.has_key(k):
846 cell.append('%s: %s'%(k, current[k]))
847 current[k] = '(no value)'
848 else:
849 cell.append('%s: (no value)'%k)
851 else:
852 cell.append('%s: %s'%(k, str(args[k])))
853 if current.has_key(k):
854 cell[-1] += ' -> %s'%current[k]
855 current[k] = str(args[k])
857 arg_s = '<br />'.join(cell)
858 else:
859 # unkown event!!
860 comments['unknown'] = _('''<strong><em>This event is not
861 handled by the history display!</em></strong>''')
862 arg_s = '<strong><em>' + str(args) + '</em></strong>'
863 date_s = date_s.replace(' ', ' ')
864 # if the user's an itemid, figure the username (older journals
865 # have the username)
866 if dre.match(user):
867 user = self._db.user.get(user, 'username')
868 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
869 date_s, user, action, arg_s))
870 if comments:
871 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
872 for entry in comments.values():
873 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
874 l.append('</table>')
875 return '\n'.join(l)
877 def renderQueryForm(self):
878 ''' Render this item, which is a query, as a search form.
879 '''
880 # create a new request and override the specified args
881 req = HTMLRequest(self._client)
882 req.classname = self._klass.get(self._nodeid, 'klass')
883 name = self._klass.get(self._nodeid, 'name')
884 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
885 '&@queryname=%s'%urllib.quote(name))
887 # new template, using the specified classname and request
888 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
890 # use our fabricated request
891 return pt.render(self._client, req.classname, req)
893 class HTMLUserPermission:
895 def is_edit_ok(self):
896 ''' Is the user allowed to Edit the current class?
897 Also check whether this is the current user's info.
898 '''
899 return self._user_perm_check('Edit')
901 def is_view_ok(self):
902 ''' Is the user allowed to View the current class?
903 Also check whether this is the current user's info.
904 '''
905 return self._user_perm_check('View')
907 def _user_perm_check(self, type):
908 # some users may view / edit all users
909 s = self._db.security
910 userid = self._client.userid
911 if s.hasPermission(type, userid, self._classname):
912 return 1
914 # users may view their own info
915 is_anonymous = self._db.user.get(userid, 'username') == 'anonymous'
916 if getattr(self, '_nodeid', None) == userid and not is_anonymous:
917 return 1
919 # may anonymous users register?
920 if (is_anonymous and s.hasPermission('Web Registration', userid,
921 self._classname)):
922 return 1
924 # nope, no access here
925 return 0
927 class HTMLUserClass(HTMLUserPermission, HTMLClass):
928 pass
930 class HTMLUser(HTMLUserPermission, HTMLItem):
931 ''' Accesses through the *user* (a special case of item)
932 '''
933 def __init__(self, client, classname, nodeid, anonymous=0):
934 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
935 self._default_classname = client.classname
937 # used for security checks
938 self._security = client.db.security
940 _marker = []
941 def hasPermission(self, permission, classname=_marker):
942 ''' Determine if the user has the Permission.
944 The class being tested defaults to the template's class, but may
945 be overidden for this test by suppling an alternate classname.
946 '''
947 if classname is self._marker:
948 classname = self._default_classname
949 return self._security.hasPermission(permission, self._nodeid, classname)
951 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
952 ''' String, Number, Date, Interval HTMLProperty
954 Has useful attributes:
956 _name the name of the property
957 _value the value of the property if any
959 A wrapper object which may be stringified for the plain() behaviour.
960 '''
961 def __init__(self, client, classname, nodeid, prop, name, value,
962 anonymous=0):
963 self._client = client
964 self._db = client.db
965 self._classname = classname
966 self._nodeid = nodeid
967 self._prop = prop
968 self._value = value
969 self._anonymous = anonymous
970 self._name = name
971 if not anonymous:
972 self._formname = '%s%s@%s'%(classname, nodeid, name)
973 else:
974 self._formname = name
976 HTMLInputMixin.__init__(self)
978 def __repr__(self):
979 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
980 self._prop, self._value)
981 def __str__(self):
982 return self.plain()
983 def __cmp__(self, other):
984 if isinstance(other, HTMLProperty):
985 return cmp(self._value, other._value)
986 return cmp(self._value, other)
988 def is_edit_ok(self):
989 ''' Is the user allowed to Edit the current class?
990 '''
991 thing = HTMLDatabase(self._client)[self._classname]
992 if self._nodeid:
993 # this is a special-case for the User class where permission's
994 # on a per-item basis :(
995 thing = thing.getItem(self._nodeid)
996 return thing.is_edit_ok()
998 def is_view_ok(self):
999 ''' Is the user allowed to View the current class?
1000 '''
1001 thing = HTMLDatabase(self._client)[self._classname]
1002 if self._nodeid:
1003 # this is a special-case for the User class where permission's
1004 # on a per-item basis :(
1005 thing = thing.getItem(self._nodeid)
1006 return thing.is_view_ok()
1008 class StringHTMLProperty(HTMLProperty):
1009 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
1010 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
1011 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
1012 def _hyper_repl(self, match):
1013 if match.group('url'):
1014 s = match.group('url')
1015 return '<a href="%s">%s</a>'%(s, s)
1016 elif match.group('email'):
1017 s = match.group('email')
1018 return '<a href="mailto:%s">%s</a>'%(s, s)
1019 else:
1020 s = match.group('item')
1021 s1 = match.group('class')
1022 s2 = match.group('id')
1023 try:
1024 # make sure s1 is a valid tracker classname
1025 cl = self._db.getclass(s1)
1026 if not cl.hasnode(s2):
1027 raise KeyError, 'oops'
1028 return '<a href="%s">%s%s</a>'%(s, s1, s2)
1029 except KeyError:
1030 return '%s%s'%(s1, s2)
1032 def hyperlinked(self):
1033 ''' Render a "hyperlinked" version of the text '''
1034 return self.plain(hyperlink=1)
1036 def plain(self, escape=0, hyperlink=0):
1037 '''Render a "plain" representation of the property
1039 - "escape" turns on/off HTML quoting
1040 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1041 addresses and designators
1042 '''
1043 self.view_check()
1045 if self._value is None:
1046 return ''
1047 if escape:
1048 s = cgi.escape(str(self._value))
1049 else:
1050 s = str(self._value)
1051 if hyperlink:
1052 # no, we *must* escape this text
1053 if not escape:
1054 s = cgi.escape(s)
1055 s = self.hyper_re.sub(self._hyper_repl, s)
1056 return s
1058 def stext(self, escape=0):
1059 ''' Render the value of the property as StructuredText.
1061 This requires the StructureText module to be installed separately.
1062 '''
1063 self.view_check()
1065 s = self.plain(escape=escape)
1066 if not StructuredText:
1067 return s
1068 return StructuredText(s,level=1,header=0)
1070 def field(self, size = 30):
1071 ''' Render the property as a field in HTML.
1073 If not editable, just display the value via plain().
1074 '''
1075 self.view_check()
1077 if self._value is None:
1078 value = ''
1079 else:
1080 value = cgi.escape(str(self._value))
1082 if self.is_edit_ok():
1083 value = '"'.join(value.split('"'))
1084 return self.input(name=self._formname,value=value,size=size)
1086 return self.plain()
1088 def multiline(self, escape=0, rows=5, cols=40):
1089 ''' Render a multiline form edit field for the property.
1091 If not editable, just display the plain() value in a <pre> tag.
1092 '''
1093 self.view_check()
1095 if self._value is None:
1096 value = ''
1097 else:
1098 value = cgi.escape(str(self._value))
1100 if self.is_edit_ok():
1101 value = '"'.join(value.split('"'))
1102 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1103 self._formname, rows, cols, value)
1105 return '<pre>%s</pre>'%self.plain()
1107 def email(self, escape=1):
1108 ''' Render the value of the property as an obscured email address
1109 '''
1110 self.view_check()
1112 if self._value is None:
1113 value = ''
1114 else:
1115 value = str(self._value)
1116 if value.find('@') != -1:
1117 name, domain = value.split('@')
1118 domain = ' '.join(domain.split('.')[:-1])
1119 name = name.replace('.', ' ')
1120 value = '%s at %s ...'%(name, domain)
1121 else:
1122 value = value.replace('.', ' ')
1123 if escape:
1124 value = cgi.escape(value)
1125 return value
1127 class PasswordHTMLProperty(HTMLProperty):
1128 def plain(self):
1129 ''' Render a "plain" representation of the property
1130 '''
1131 self.view_check()
1133 if self._value is None:
1134 return ''
1135 return _('*encrypted*')
1137 def field(self, size = 30):
1138 ''' Render a form edit field for the property.
1140 If not editable, just display the value via plain().
1141 '''
1142 self.view_check()
1144 if self.is_edit_ok():
1145 return self.input(type="password", name=self._formname, size=size)
1147 return self.plain()
1149 def confirm(self, size = 30):
1150 ''' Render a second form edit field for the property, used for
1151 confirmation that the user typed the password correctly. Generates
1152 a field with name "@confirm@name".
1154 If not editable, display nothing.
1155 '''
1156 self.view_check()
1158 if self.is_edit_ok():
1159 return self.input(type="password",
1160 name="@confirm@%s"%self._formname, size=size)
1162 return ''
1164 class NumberHTMLProperty(HTMLProperty):
1165 def plain(self):
1166 ''' Render a "plain" representation of the property
1167 '''
1168 self.view_check()
1170 return str(self._value)
1172 def field(self, size = 30):
1173 ''' Render a form edit field for the property.
1175 If not editable, just display the value via plain().
1176 '''
1177 self.view_check()
1179 if self._value is None:
1180 value = ''
1181 else:
1182 value = cgi.escape(str(self._value))
1184 if self.is_edit_ok():
1185 value = '"'.join(value.split('"'))
1186 return self.input(name=self._formname,value=value,size=size)
1188 return self.plain()
1190 def __int__(self):
1191 ''' Return an int of me
1192 '''
1193 return int(self._value)
1195 def __float__(self):
1196 ''' Return a float of me
1197 '''
1198 return float(self._value)
1201 class BooleanHTMLProperty(HTMLProperty):
1202 def plain(self):
1203 ''' Render a "plain" representation of the property
1204 '''
1205 self.view_check()
1207 if self._value is None:
1208 return ''
1209 return self._value and "Yes" or "No"
1211 def field(self):
1212 ''' Render a form edit field for the property
1214 If not editable, just display the value via plain().
1215 '''
1216 self.view_check()
1218 if not self.is_edit_ok():
1219 return self.plain()
1221 checked = self._value and "checked" or ""
1222 if self._value:
1223 s = self.input(type="radio", name=self._formname, value="yes",
1224 checked="checked")
1225 s += 'Yes'
1226 s +=self.input(type="radio", name=self._formname, value="no")
1227 s += 'No'
1228 else:
1229 s = self.input(type="radio", name=self._formname, value="yes")
1230 s += 'Yes'
1231 s +=self.input(type="radio", name=self._formname, value="no",
1232 checked="checked")
1233 s += 'No'
1234 return s
1236 class DateHTMLProperty(HTMLProperty):
1237 def plain(self):
1238 ''' Render a "plain" representation of the property
1239 '''
1240 self.view_check()
1242 if self._value is None:
1243 return ''
1244 return str(self._value.local(self._db.getUserTimezone()))
1246 def now(self):
1247 ''' Return the current time.
1249 This is useful for defaulting a new value. Returns a
1250 DateHTMLProperty.
1251 '''
1252 self.view_check()
1254 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1255 self._prop, self._formname, date.Date('.'))
1257 def field(self, size = 30):
1258 ''' Render a form edit field for the property
1260 If not editable, just display the value via plain().
1261 '''
1262 self.view_check()
1264 if self._value is None:
1265 value = ''
1266 else:
1267 tz = self._db.getUserTimezone()
1268 value = cgi.escape(str(self._value.local(tz)))
1270 if self.is_edit_ok():
1271 value = '"'.join(value.split('"'))
1272 return self.input(name=self._formname,value=value,size=size)
1274 return self.plain()
1276 def reldate(self, pretty=1):
1277 ''' Render the interval between the date and now.
1279 If the "pretty" flag is true, then make the display pretty.
1280 '''
1281 self.view_check()
1283 if not self._value:
1284 return ''
1286 # figure the interval
1287 interval = self._value - date.Date('.')
1288 if pretty:
1289 return interval.pretty()
1290 return str(interval)
1292 _marker = []
1293 def pretty(self, format=_marker):
1294 ''' Render the date in a pretty format (eg. month names, spaces).
1296 The format string is a standard python strftime format string.
1297 Note that if the day is zero, and appears at the start of the
1298 string, then it'll be stripped from the output. This is handy
1299 for the situatin when a date only specifies a month and a year.
1300 '''
1301 self.view_check()
1303 if format is not self._marker:
1304 return self._value.pretty(format)
1305 else:
1306 return self._value.pretty()
1308 def local(self, offset):
1309 ''' Return the date/time as a local (timezone offset) date/time.
1310 '''
1311 self.view_check()
1313 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1314 self._prop, self._formname, self._value.local(offset))
1316 class IntervalHTMLProperty(HTMLProperty):
1317 def plain(self):
1318 ''' Render a "plain" representation of the property
1319 '''
1320 self.view_check()
1322 if self._value is None:
1323 return ''
1324 return str(self._value)
1326 def pretty(self):
1327 ''' Render the interval in a pretty format (eg. "yesterday")
1328 '''
1329 self.view_check()
1331 return self._value.pretty()
1333 def field(self, size = 30):
1334 ''' Render a form edit field for the property
1336 If not editable, just display the value via plain().
1337 '''
1338 self.view_check()
1340 if self._value is None:
1341 value = ''
1342 else:
1343 value = cgi.escape(str(self._value))
1345 if is_edit_ok():
1346 value = '"'.join(value.split('"'))
1347 return self.input(name=self._formname,value=value,size=size)
1349 return self.plain()
1351 class LinkHTMLProperty(HTMLProperty):
1352 ''' Link HTMLProperty
1353 Include the above as well as being able to access the class
1354 information. Stringifying the object itself results in the value
1355 from the item being displayed. Accessing attributes of this object
1356 result in the appropriate entry from the class being queried for the
1357 property accessed (so item/assignedto/name would look up the user
1358 entry identified by the assignedto property on item, and then the
1359 name property of that user)
1360 '''
1361 def __init__(self, *args, **kw):
1362 HTMLProperty.__init__(self, *args, **kw)
1363 # if we're representing a form value, then the -1 from the form really
1364 # should be a None
1365 if str(self._value) == '-1':
1366 self._value = None
1368 def __getattr__(self, attr):
1369 ''' return a new HTMLItem '''
1370 #print 'Link.getattr', (self, attr, self._value)
1371 if not self._value:
1372 raise AttributeError, "Can't access missing value"
1373 if self._prop.classname == 'user':
1374 klass = HTMLUser
1375 else:
1376 klass = HTMLItem
1377 i = klass(self._client, self._prop.classname, self._value)
1378 return getattr(i, attr)
1380 def plain(self, escape=0):
1381 ''' Render a "plain" representation of the property
1382 '''
1383 self.view_check()
1385 if self._value is None:
1386 return ''
1387 linkcl = self._db.classes[self._prop.classname]
1388 k = linkcl.labelprop(1)
1389 value = str(linkcl.get(self._value, k))
1390 if escape:
1391 value = cgi.escape(value)
1392 return value
1394 def field(self, showid=0, size=None):
1395 ''' Render a form edit field for the property
1397 If not editable, just display the value via plain().
1398 '''
1399 self.view_check()
1401 if not self.is_edit_ok():
1402 return self.plain()
1404 # edit field
1405 linkcl = self._db.getclass(self._prop.classname)
1406 if self._value is None:
1407 value = ''
1408 else:
1409 k = linkcl.getkey()
1410 if k:
1411 value = linkcl.get(self._value, k)
1412 else:
1413 value = self._value
1414 value = cgi.escape(str(value))
1415 value = '"'.join(value.split('"'))
1416 return '<input name="%s" value="%s" size="%s">'%(self._formname,
1417 value, size)
1419 def menu(self, size=None, height=None, showid=0, additional=[],
1420 sort_on=None, **conditions):
1421 ''' Render a form select list for this property
1423 If not editable, just display the value via plain().
1424 '''
1425 self.view_check()
1427 if not self.is_edit_ok():
1428 return self.plain()
1430 value = self._value
1432 linkcl = self._db.getclass(self._prop.classname)
1433 l = ['<select name="%s">'%self._formname]
1434 k = linkcl.labelprop(1)
1435 s = ''
1436 if value is None:
1437 s = 'selected="selected" '
1438 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1439 if linkcl.getprops().has_key('order'):
1440 sort_on = ('+', 'order')
1441 else:
1442 if sort_on is None:
1443 sort_on = ('+', linkcl.labelprop())
1444 else:
1445 sort_on = ('+', sort_on)
1446 options = linkcl.filter(None, conditions, sort_on, (None, None))
1448 # make sure we list the current value if it's retired
1449 if self._value and self._value not in options:
1450 options.insert(0, self._value)
1452 for optionid in options:
1453 # get the option value, and if it's None use an empty string
1454 option = linkcl.get(optionid, k) or ''
1456 # figure if this option is selected
1457 s = ''
1458 if value in [optionid, option]:
1459 s = 'selected="selected" '
1461 # figure the label
1462 if showid:
1463 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1464 else:
1465 lab = option
1467 # truncate if it's too long
1468 if size is not None and len(lab) > size:
1469 lab = lab[:size-3] + '...'
1470 if additional:
1471 m = []
1472 for propname in additional:
1473 m.append(linkcl.get(optionid, propname))
1474 lab = lab + ' (%s)'%', '.join(map(str, m))
1476 # and generate
1477 lab = cgi.escape(lab)
1478 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1479 l.append('</select>')
1480 return '\n'.join(l)
1481 # def checklist(self, ...)
1483 class MultilinkHTMLProperty(HTMLProperty):
1484 ''' Multilink HTMLProperty
1486 Also be iterable, returning a wrapper object like the Link case for
1487 each entry in the multilink.
1488 '''
1489 def __init__(self, *args, **kwargs):
1490 HTMLProperty.__init__(self, *args, **kwargs)
1491 if self._value:
1492 sortfun = make_sort_function(self._db, self._prop.classname)
1493 self._value.sort(sortfun)
1495 def __len__(self):
1496 ''' length of the multilink '''
1497 return len(self._value)
1499 def __getattr__(self, attr):
1500 ''' no extended attribute accesses make sense here '''
1501 raise AttributeError, attr
1503 def __getitem__(self, num):
1504 ''' iterate and return a new HTMLItem
1505 '''
1506 #print 'Multi.getitem', (self, num)
1507 value = self._value[num]
1508 if self._prop.classname == 'user':
1509 klass = HTMLUser
1510 else:
1511 klass = HTMLItem
1512 return klass(self._client, self._prop.classname, value)
1514 def __contains__(self, value):
1515 ''' Support the "in" operator. We have to make sure the passed-in
1516 value is a string first, not a HTMLProperty.
1517 '''
1518 return str(value) in self._value
1520 def reverse(self):
1521 ''' return the list in reverse order
1522 '''
1523 l = self._value[:]
1524 l.reverse()
1525 if self._prop.classname == 'user':
1526 klass = HTMLUser
1527 else:
1528 klass = HTMLItem
1529 return [klass(self._client, self._prop.classname, value) for value in l]
1531 def plain(self, escape=0):
1532 ''' Render a "plain" representation of the property
1533 '''
1534 self.view_check()
1536 linkcl = self._db.classes[self._prop.classname]
1537 k = linkcl.labelprop(1)
1538 labels = []
1539 for v in self._value:
1540 labels.append(linkcl.get(v, k))
1541 value = ', '.join(labels)
1542 if escape:
1543 value = cgi.escape(value)
1544 return value
1546 def field(self, size=30, showid=0):
1547 ''' Render a form edit field for the property
1549 If not editable, just display the value via plain().
1550 '''
1551 self.view_check()
1553 if not self.is_edit_ok():
1554 return self.plain()
1556 linkcl = self._db.getclass(self._prop.classname)
1557 value = self._value[:]
1558 # map the id to the label property
1559 if not linkcl.getkey():
1560 showid=1
1561 if not showid:
1562 k = linkcl.labelprop(1)
1563 value = lookupKeys(linkcl, k, value)
1564 value = cgi.escape(','.join(value))
1565 return self.input(name=self._formname,size=size,value=value)
1567 def menu(self, size=None, height=None, showid=0, additional=[],
1568 sort_on=None, **conditions):
1569 ''' Render a form select list for this property
1571 If not editable, just display the value via plain().
1572 '''
1573 self.view_check()
1575 if not self.is_edit_ok():
1576 return self.plain()
1578 value = self._value
1580 linkcl = self._db.getclass(self._prop.classname)
1581 if sort_on is None:
1582 sort_on = ('+', find_sort_key(linkcl))
1583 else:
1584 sort_on = ('+', sort_on)
1585 options = linkcl.filter(None, conditions, sort_on)
1586 height = height or min(len(options), 7)
1587 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1588 k = linkcl.labelprop(1)
1590 # make sure we list the current values if they're retired
1591 for val in value:
1592 if val not in options:
1593 options.insert(0, val)
1595 for optionid in options:
1596 # get the option value, and if it's None use an empty string
1597 option = linkcl.get(optionid, k) or ''
1599 # figure if this option is selected
1600 s = ''
1601 if optionid in value or option in value:
1602 s = 'selected="selected" '
1604 # figure the label
1605 if showid:
1606 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1607 else:
1608 lab = option
1609 # truncate if it's too long
1610 if size is not None and len(lab) > size:
1611 lab = lab[:size-3] + '...'
1612 if additional:
1613 m = []
1614 for propname in additional:
1615 m.append(linkcl.get(optionid, propname))
1616 lab = lab + ' (%s)'%', '.join(m)
1618 # and generate
1619 lab = cgi.escape(lab)
1620 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1621 lab))
1622 l.append('</select>')
1623 return '\n'.join(l)
1625 # set the propclasses for HTMLItem
1626 propclasses = (
1627 (hyperdb.String, StringHTMLProperty),
1628 (hyperdb.Number, NumberHTMLProperty),
1629 (hyperdb.Boolean, BooleanHTMLProperty),
1630 (hyperdb.Date, DateHTMLProperty),
1631 (hyperdb.Interval, IntervalHTMLProperty),
1632 (hyperdb.Password, PasswordHTMLProperty),
1633 (hyperdb.Link, LinkHTMLProperty),
1634 (hyperdb.Multilink, MultilinkHTMLProperty),
1635 )
1637 def make_sort_function(db, classname, sort_on=None):
1638 '''Make a sort function for a given class
1639 '''
1640 linkcl = db.getclass(classname)
1641 if sort_on is None:
1642 sort_on = find_sort_key(linkcl)
1643 def sortfunc(a, b):
1644 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1645 return sortfunc
1647 def find_sort_key(linkcl):
1648 if linkcl.getprops().has_key('order'):
1649 return 'order'
1650 else:
1651 return linkcl.labelprop()
1653 def handleListCGIValue(value):
1654 ''' Value is either a single item or a list of items. Each item has a
1655 .value that we're actually interested in.
1656 '''
1657 if isinstance(value, type([])):
1658 return [value.value for value in value]
1659 else:
1660 value = value.value.strip()
1661 if not value:
1662 return []
1663 return value.split(',')
1665 class ShowDict:
1666 ''' A convenience access to the :columns index parameters
1667 '''
1668 def __init__(self, columns):
1669 self.columns = {}
1670 for col in columns:
1671 self.columns[col] = 1
1672 def __getitem__(self, name):
1673 return self.columns.has_key(name)
1675 class HTMLRequest(HTMLInputMixin):
1676 '''The *request*, holding the CGI form and environment.
1678 - "form" the CGI form as a cgi.FieldStorage
1679 - "env" the CGI environment variables
1680 - "base" the base URL for this instance
1681 - "user" a HTMLUser instance for this user
1682 - "classname" the current classname (possibly None)
1683 - "template" the current template (suffix, also possibly None)
1685 Index args:
1687 - "columns" dictionary of the columns to display in an index page
1688 - "show" a convenience access to columns - request/show/colname will
1689 be true if the columns should be displayed, false otherwise
1690 - "sort" index sort column (direction, column name)
1691 - "group" index grouping property (direction, column name)
1692 - "filter" properties to filter the index on
1693 - "filterspec" values to filter the index on
1694 - "search_text" text to perform a full-text search on for an index
1695 '''
1696 def __init__(self, client):
1697 # _client is needed by HTMLInputMixin
1698 self._client = self.client = client
1700 # easier access vars
1701 self.form = client.form
1702 self.env = client.env
1703 self.base = client.base
1704 self.user = HTMLUser(client, 'user', client.userid)
1706 # store the current class name and action
1707 self.classname = client.classname
1708 self.template = client.template
1710 # the special char to use for special vars
1711 self.special_char = '@'
1713 HTMLInputMixin.__init__(self)
1715 self._post_init()
1717 def _post_init(self):
1718 ''' Set attributes based on self.form
1719 '''
1720 # extract the index display information from the form
1721 self.columns = []
1722 for name in ':columns @columns'.split():
1723 if self.form.has_key(name):
1724 self.special_char = name[0]
1725 self.columns = handleListCGIValue(self.form[name])
1726 break
1727 self.show = ShowDict(self.columns)
1729 # sorting
1730 self.sort = (None, None)
1731 for name in ':sort @sort'.split():
1732 if self.form.has_key(name):
1733 self.special_char = name[0]
1734 sort = self.form[name].value
1735 if sort.startswith('-'):
1736 self.sort = ('-', sort[1:])
1737 else:
1738 self.sort = ('+', sort)
1739 if self.form.has_key(self.special_char+'sortdir'):
1740 self.sort = ('-', self.sort[1])
1742 # grouping
1743 self.group = (None, None)
1744 for name in ':group @group'.split():
1745 if self.form.has_key(name):
1746 self.special_char = name[0]
1747 group = self.form[name].value
1748 if group.startswith('-'):
1749 self.group = ('-', group[1:])
1750 else:
1751 self.group = ('+', group)
1752 if self.form.has_key(self.special_char+'groupdir'):
1753 self.group = ('-', self.group[1])
1755 # filtering
1756 self.filter = []
1757 for name in ':filter @filter'.split():
1758 if self.form.has_key(name):
1759 self.special_char = name[0]
1760 self.filter = handleListCGIValue(self.form[name])
1762 self.filterspec = {}
1763 db = self.client.db
1764 if self.classname is not None:
1765 props = db.getclass(self.classname).getprops()
1766 for name in self.filter:
1767 if not self.form.has_key(name):
1768 continue
1769 prop = props[name]
1770 fv = self.form[name]
1771 if (isinstance(prop, hyperdb.Link) or
1772 isinstance(prop, hyperdb.Multilink)):
1773 self.filterspec[name] = lookupIds(db, prop,
1774 handleListCGIValue(fv))
1775 else:
1776 if isinstance(fv, type([])):
1777 self.filterspec[name] = [v.value for v in fv]
1778 else:
1779 self.filterspec[name] = fv.value
1781 # full-text search argument
1782 self.search_text = None
1783 for name in ':search_text @search_text'.split():
1784 if self.form.has_key(name):
1785 self.special_char = name[0]
1786 self.search_text = self.form[name].value
1788 # pagination - size and start index
1789 # figure batch args
1790 self.pagesize = 50
1791 for name in ':pagesize @pagesize'.split():
1792 if self.form.has_key(name):
1793 self.special_char = name[0]
1794 self.pagesize = int(self.form[name].value)
1796 self.startwith = 0
1797 for name in ':startwith @startwith'.split():
1798 if self.form.has_key(name):
1799 self.special_char = name[0]
1800 self.startwith = int(self.form[name].value)
1802 def updateFromURL(self, url):
1803 ''' Parse the URL for query args, and update my attributes using the
1804 values.
1805 '''
1806 env = {'QUERY_STRING': url}
1807 self.form = cgi.FieldStorage(environ=env)
1809 self._post_init()
1811 def update(self, kwargs):
1812 ''' Update my attributes using the keyword args
1813 '''
1814 self.__dict__.update(kwargs)
1815 if kwargs.has_key('columns'):
1816 self.show = ShowDict(self.columns)
1818 def description(self):
1819 ''' Return a description of the request - handle for the page title.
1820 '''
1821 s = [self.client.db.config.TRACKER_NAME]
1822 if self.classname:
1823 if self.client.nodeid:
1824 s.append('- %s%s'%(self.classname, self.client.nodeid))
1825 else:
1826 if self.template == 'item':
1827 s.append('- new %s'%self.classname)
1828 elif self.template == 'index':
1829 s.append('- %s index'%self.classname)
1830 else:
1831 s.append('- %s %s'%(self.classname, self.template))
1832 else:
1833 s.append('- home')
1834 return ' '.join(s)
1836 def __str__(self):
1837 d = {}
1838 d.update(self.__dict__)
1839 f = ''
1840 for k in self.form.keys():
1841 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1842 d['form'] = f
1843 e = ''
1844 for k,v in self.env.items():
1845 e += '\n %r=%r'%(k, v)
1846 d['env'] = e
1847 return '''
1848 form: %(form)s
1849 base: %(base)r
1850 classname: %(classname)r
1851 template: %(template)r
1852 columns: %(columns)r
1853 sort: %(sort)r
1854 group: %(group)r
1855 filter: %(filter)r
1856 search_text: %(search_text)r
1857 pagesize: %(pagesize)r
1858 startwith: %(startwith)r
1859 env: %(env)s
1860 '''%d
1862 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1863 filterspec=1):
1864 ''' return the current index args as form elements '''
1865 l = []
1866 sc = self.special_char
1867 s = self.input(type="hidden",name="%s",value="%s")
1868 if columns and self.columns:
1869 l.append(s%(sc+'columns', ','.join(self.columns)))
1870 if sort and self.sort[1] is not None:
1871 if self.sort[0] == '-':
1872 val = '-'+self.sort[1]
1873 else:
1874 val = self.sort[1]
1875 l.append(s%(sc+'sort', val))
1876 if group and self.group[1] is not None:
1877 if self.group[0] == '-':
1878 val = '-'+self.group[1]
1879 else:
1880 val = self.group[1]
1881 l.append(s%(sc+'group', val))
1882 if filter and self.filter:
1883 l.append(s%(sc+'filter', ','.join(self.filter)))
1884 if filterspec:
1885 for k,v in self.filterspec.items():
1886 if type(v) == type([]):
1887 l.append(s%(k, ','.join(v)))
1888 else:
1889 l.append(s%(k, v))
1890 if self.search_text:
1891 l.append(s%(sc+'search_text', self.search_text))
1892 l.append(s%(sc+'pagesize', self.pagesize))
1893 l.append(s%(sc+'startwith', self.startwith))
1894 return '\n'.join(l)
1896 def indexargs_url(self, url, args):
1897 ''' Embed the current index args in a URL
1898 '''
1899 sc = self.special_char
1900 l = ['%s=%s'%(k,v) for k,v in args.items()]
1902 # pull out the special values (prefixed by @ or :)
1903 specials = {}
1904 for key in args.keys():
1905 if key[0] in '@:':
1906 specials[key[1:]] = args[key]
1908 # ok, now handle the specials we received in the request
1909 if self.columns and not specials.has_key('columns'):
1910 l.append(sc+'columns=%s'%(','.join(self.columns)))
1911 if self.sort[1] is not None and not specials.has_key('sort'):
1912 if self.sort[0] == '-':
1913 val = '-'+self.sort[1]
1914 else:
1915 val = self.sort[1]
1916 l.append(sc+'sort=%s'%val)
1917 if self.group[1] is not None and not specials.has_key('group'):
1918 if self.group[0] == '-':
1919 val = '-'+self.group[1]
1920 else:
1921 val = self.group[1]
1922 l.append(sc+'group=%s'%val)
1923 if self.filter and not specials.has_key('filter'):
1924 l.append(sc+'filter=%s'%(','.join(self.filter)))
1925 if self.search_text and not specials.has_key('search_text'):
1926 l.append(sc+'search_text=%s'%self.search_text)
1927 if not specials.has_key('pagesize'):
1928 l.append(sc+'pagesize=%s'%self.pagesize)
1929 if not specials.has_key('startwith'):
1930 l.append(sc+'startwith=%s'%self.startwith)
1932 # finally, the remainder of the filter args in the request
1933 for k,v in self.filterspec.items():
1934 if not args.has_key(k):
1935 if type(v) == type([]):
1936 l.append('%s=%s'%(k, ','.join(v)))
1937 else:
1938 l.append('%s=%s'%(k, v))
1939 return '%s?%s'%(url, '&'.join(l))
1940 indexargs_href = indexargs_url
1942 def base_javascript(self):
1943 return '''
1944 <script type="text/javascript">
1945 submitted = false;
1946 function submit_once() {
1947 if (submitted) {
1948 alert("Your request is being processed.\\nPlease be patient.");
1949 event.returnValue = 0; // work-around for IE
1950 return 0;
1951 }
1952 submitted = true;
1953 return 1;
1954 }
1956 function help_window(helpurl, width, height) {
1957 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1958 }
1959 </script>
1960 '''%self.base
1962 def batch(self):
1963 ''' Return a batch object for results from the "current search"
1964 '''
1965 filterspec = self.filterspec
1966 sort = self.sort
1967 group = self.group
1969 # get the list of ids we're batching over
1970 klass = self.client.db.getclass(self.classname)
1971 if self.search_text:
1972 matches = self.client.db.indexer.search(
1973 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1974 else:
1975 matches = None
1976 l = klass.filter(matches, filterspec, sort, group)
1978 # return the batch object, using IDs only
1979 return Batch(self.client, l, self.pagesize, self.startwith,
1980 classname=self.classname)
1982 # extend the standard ZTUtils Batch object to remove dependency on
1983 # Acquisition and add a couple of useful methods
1984 class Batch(ZTUtils.Batch):
1985 ''' Use me to turn a list of items, or item ids of a given class, into a
1986 series of batches.
1988 ========= ========================================================
1989 Parameter Usage
1990 ========= ========================================================
1991 sequence a list of HTMLItems or item ids
1992 classname if sequence is a list of ids, this is the class of item
1993 size how big to make the sequence.
1994 start where to start (0-indexed) in the sequence.
1995 end where to end (0-indexed) in the sequence.
1996 orphan if the next batch would contain less items than this
1997 value, then it is combined with this batch
1998 overlap the number of items shared between adjacent batches
1999 ========= ========================================================
2001 Attributes: Note that the "start" attribute, unlike the
2002 argument, is a 1-based index (I know, lame). "first" is the
2003 0-based index. "length" is the actual number of elements in
2004 the batch.
2006 "sequence_length" is the length of the original, unbatched, sequence.
2007 '''
2008 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2009 overlap=0, classname=None):
2010 self.client = client
2011 self.last_index = self.last_item = None
2012 self.current_item = None
2013 self.classname = classname
2014 self.sequence_length = len(sequence)
2015 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2016 overlap)
2018 # overwrite so we can late-instantiate the HTMLItem instance
2019 def __getitem__(self, index):
2020 if index < 0:
2021 if index + self.end < self.first: raise IndexError, index
2022 return self._sequence[index + self.end]
2024 if index >= self.length:
2025 raise IndexError, index
2027 # move the last_item along - but only if the fetched index changes
2028 # (for some reason, index 0 is fetched twice)
2029 if index != self.last_index:
2030 self.last_item = self.current_item
2031 self.last_index = index
2033 item = self._sequence[index + self.first]
2034 if self.classname:
2035 # map the item ids to instances
2036 if self.classname == 'user':
2037 item = HTMLUser(self.client, self.classname, item)
2038 else:
2039 item = HTMLItem(self.client, self.classname, item)
2040 self.current_item = item
2041 return item
2043 def propchanged(self, property):
2044 ''' Detect if the property marked as being the group property
2045 changed in the last iteration fetch
2046 '''
2047 if (self.last_item is None or
2048 self.last_item[property] != self.current_item[property]):
2049 return 1
2050 return 0
2052 # override these 'cos we don't have access to acquisition
2053 def previous(self):
2054 if self.start == 1:
2055 return None
2056 return Batch(self.client, self._sequence, self._size,
2057 self.first - self._size + self.overlap, 0, self.orphan,
2058 self.overlap)
2060 def next(self):
2061 try:
2062 self._sequence[self.end]
2063 except IndexError:
2064 return None
2065 return Batch(self.client, self._sequence, self._size,
2066 self.end - self.overlap, 0, self.orphan, self.overlap)
2068 class TemplatingUtils:
2069 ''' Utilities for templating
2070 '''
2071 def __init__(self, client):
2072 self.client = client
2073 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2074 return Batch(self.client, sequence, size, start, end, orphan,
2075 overlap)