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 self._client.db.getclass(m.group('cl'))
257 return HTMLItem(self._client, m.group('cl'), m.group('id'))
258 else:
259 self._client.db.getclass(item)
260 if item == 'user':
261 return HTMLUserClass(self._client, item)
262 return HTMLClass(self._client, item)
264 def __getattr__(self, attr):
265 try:
266 return self[attr]
267 except KeyError:
268 raise AttributeError, attr
270 def classes(self):
271 l = self._client.db.classes.keys()
272 l.sort()
273 m = []
274 for item in l:
275 if item == 'user':
276 m.append(HTMLUserClass(self._client, item))
277 m.append(HTMLClass(self._client, item))
278 return m
280 def lookupIds(db, prop, ids, fail_ok=0, num_re=re.compile('-?\d+')):
281 ''' "fail_ok" should be specified if we wish to pass through bad values
282 (most likely form values that we wish to represent back to the user)
283 '''
284 cl = db.getclass(prop.classname)
285 l = []
286 for entry in ids:
287 if num_re.match(entry):
288 l.append(entry)
289 else:
290 try:
291 l.append(cl.lookup(entry))
292 except (TypeError, KeyError):
293 if fail_ok:
294 # pass through the bad value
295 l.append(entry)
296 return l
298 def lookupKeys(linkcl, key, ids, num_re=re.compile('-?\d+')):
299 ''' Look up the "key" values for "ids" list - though some may already
300 be key values, not ids.
301 '''
302 l = []
303 for entry in ids:
304 if num_re.match(entry):
305 l.append(linkcl.get(entry, key))
306 else:
307 l.append(entry)
308 return l
310 class HTMLPermissions:
311 ''' Helpers that provide answers to commonly asked Permission questions.
312 '''
313 def is_edit_ok(self):
314 ''' Is the user allowed to Edit the current class?
315 '''
316 return self._db.security.hasPermission('Edit', self._client.userid,
317 self._classname)
319 def is_view_ok(self):
320 ''' Is the user allowed to View the current class?
321 '''
322 return self._db.security.hasPermission('View', self._client.userid,
323 self._classname)
325 def is_only_view_ok(self):
326 ''' Is the user only allowed to View (ie. not Edit) the current class?
327 '''
328 return self.is_view_ok() and not self.is_edit_ok()
330 def view_check(self):
331 ''' Raise the Unauthorised exception if the user's not permitted to
332 view this class.
333 '''
334 if not self.is_view_ok():
335 raise Unauthorised("view", self._classname)
337 def edit_check(self):
338 ''' Raise the Unauthorised exception if the user's not permitted to
339 edit this class.
340 '''
341 if not self.is_edit_ok():
342 raise Unauthorised("edit", self._classname)
344 def input_html4(**attrs):
345 """Generate an 'input' (html4) element with given attributes"""
346 return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
348 def input_xhtml(**attrs):
349 """Generate an 'input' (xhtml) element with given attributes"""
350 return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
352 class HTMLInputMixin:
353 ''' requires a _client property '''
354 def __init__(self):
355 html_version = 'html4'
356 if hasattr(self._client.instance.config, 'HTML_VERSION'):
357 html_version = self._client.instance.config.HTML_VERSION
358 if html_version == 'xhtml':
359 self.input = input_xhtml
360 else:
361 self.input = input_html4
363 class HTMLClass(HTMLInputMixin, HTMLPermissions):
364 ''' Accesses through a class (either through *class* or *db.<classname>*)
365 '''
366 def __init__(self, client, classname, anonymous=0):
367 self._client = client
368 self._db = client.db
369 self._anonymous = anonymous
371 # we want classname to be exposed, but _classname gives a
372 # consistent API for extending Class/Item
373 self._classname = self.classname = classname
374 self._klass = self._db.getclass(self.classname)
375 self._props = self._klass.getprops()
377 HTMLInputMixin.__init__(self)
379 def __repr__(self):
380 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
382 def __getitem__(self, item):
383 ''' return an HTMLProperty instance
384 '''
385 #print 'HTMLClass.getitem', (self, item)
387 # we don't exist
388 if item == 'id':
389 return None
391 # get the property
392 try:
393 prop = self._props[item]
394 except KeyError:
395 raise KeyError, 'No such property "%s" on %s'%(item, self.classname)
397 # look up the correct HTMLProperty class
398 form = self._client.form
399 for klass, htmlklass in propclasses:
400 if not isinstance(prop, klass):
401 continue
402 if form.has_key(item):
403 if isinstance(prop, hyperdb.Multilink):
404 value = lookupIds(self._db, prop,
405 handleListCGIValue(form[item]), fail_ok=1)
406 elif isinstance(prop, hyperdb.Link):
407 value = form[item].value.strip()
408 if value:
409 value = lookupIds(self._db, prop, [value],
410 fail_ok=1)[0]
411 else:
412 value = None
413 else:
414 value = form[item].value.strip() or None
415 else:
416 if isinstance(prop, hyperdb.Multilink):
417 value = []
418 else:
419 value = None
420 return htmlklass(self._client, self._classname, '', prop, item,
421 value, self._anonymous)
423 # no good
424 raise KeyError, item
426 def __getattr__(self, attr):
427 ''' convenience access '''
428 try:
429 return self[attr]
430 except KeyError:
431 raise AttributeError, attr
433 def designator(self):
434 ''' Return this class' designator (classname) '''
435 return self._classname
437 def getItem(self, itemid, num_re=re.compile('-?\d+')):
438 ''' Get an item of this class by its item id.
439 '''
440 # make sure we're looking at an itemid
441 if not isinstance(itemid, type(1)) and not num_re.match(itemid):
442 itemid = self._klass.lookup(itemid)
444 if self.classname == 'user':
445 klass = HTMLUser
446 else:
447 klass = HTMLItem
449 return klass(self._client, self.classname, itemid)
451 def properties(self, sort=1):
452 ''' Return HTMLProperty for all of this class' properties.
453 '''
454 l = []
455 for name, prop in self._props.items():
456 for klass, htmlklass in propclasses:
457 if isinstance(prop, hyperdb.Multilink):
458 value = []
459 else:
460 value = None
461 if isinstance(prop, klass):
462 l.append(htmlklass(self._client, self._classname, '',
463 prop, name, value, self._anonymous))
464 if sort:
465 l.sort(lambda a,b:cmp(a._name, b._name))
466 return l
468 def list(self, sort_on=None):
469 ''' List all items in this class.
470 '''
471 if self.classname == 'user':
472 klass = HTMLUser
473 else:
474 klass = HTMLItem
476 # get the list and sort it nicely
477 l = self._klass.list()
478 sortfunc = make_sort_function(self._db, self.classname, sort_on)
479 l.sort(sortfunc)
481 l = [klass(self._client, self.classname, x) for x in l]
482 return l
484 def csv(self):
485 ''' Return the items of this class as a chunk of CSV text.
486 '''
487 if rcsv.error:
488 return rcsv.error
490 props = self.propnames()
491 s = StringIO.StringIO()
492 writer = rcsv.writer(s, rcsv.comma_separated)
493 writer.writerow(props)
494 for nodeid in self._klass.list():
495 l = []
496 for name in props:
497 value = self._klass.get(nodeid, name)
498 if value is None:
499 l.append('')
500 elif isinstance(value, type([])):
501 l.append(':'.join(map(str, value)))
502 else:
503 l.append(str(self._klass.get(nodeid, name)))
504 writer.writerow(l)
505 return s.getvalue()
507 def propnames(self):
508 ''' Return the list of the names of the properties of this class.
509 '''
510 idlessprops = self._klass.getprops(protected=0).keys()
511 idlessprops.sort()
512 return ['id'] + idlessprops
514 def filter(self, request=None, filterspec={}, sort=(None,None),
515 group=(None,None)):
516 ''' Return a list of items from this class, filtered and sorted
517 by the current requested filterspec/filter/sort/group args
519 "request" takes precedence over the other three arguments.
520 '''
521 if request is not None:
522 filterspec = request.filterspec
523 sort = request.sort
524 group = request.group
525 if self.classname == 'user':
526 klass = HTMLUser
527 else:
528 klass = HTMLItem
529 l = [klass(self._client, self.classname, x)
530 for x in self._klass.filter(None, filterspec, sort, group)]
531 return l
533 def classhelp(self, properties=None, label='(list)', width='500',
534 height='400', property=''):
535 ''' Pop up a javascript window with class help
537 This generates a link to a popup window which displays the
538 properties indicated by "properties" of the class named by
539 "classname". The "properties" should be a comma-separated list
540 (eg. 'id,name,description'). Properties defaults to all the
541 properties of a class (excluding id, creator, created and
542 activity).
544 You may optionally override the label displayed, the width and
545 height. The popup window will be resizable and scrollable.
547 If the "property" arg is given, it's passed through to the
548 javascript help_window function.
549 '''
550 if properties is None:
551 properties = self._klass.getprops(protected=0).keys()
552 properties.sort()
553 properties = ','.join(properties)
554 if property:
555 property = '&property=%s'%property
556 return '<a class="classhelp" href="javascript:help_window(\'%s?'\
557 '@startwith=0&@template=help&properties=%s%s\', \'%s\', \
558 \'%s\')">%s</a>'%(self.classname, properties, property, width,
559 height, label)
561 def submit(self, label="Submit New Entry"):
562 ''' Generate a submit button (and action hidden element)
563 '''
564 self.view_check()
565 if self.is_edit_ok():
566 return self.input(type="hidden",name="@action",value="new") + \
567 '\n' + self.input(type="submit",name="submit",value=label)
568 return ''
570 def history(self):
571 self.view_check()
572 return 'New node - no history'
574 def renderWith(self, name, **kwargs):
575 ''' Render this class with the given template.
576 '''
577 # create a new request and override the specified args
578 req = HTMLRequest(self._client)
579 req.classname = self.classname
580 req.update(kwargs)
582 # new template, using the specified classname and request
583 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
585 # use our fabricated request
586 args = {
587 'ok_message': self._client.ok_message,
588 'error_message': self._client.error_message
589 }
590 return pt.render(self._client, self.classname, req, **args)
592 class HTMLItem(HTMLInputMixin, HTMLPermissions):
593 ''' Accesses through an *item*
594 '''
595 def __init__(self, client, classname, nodeid, anonymous=0):
596 self._client = client
597 self._db = client.db
598 self._classname = classname
599 self._nodeid = nodeid
600 self._klass = self._db.getclass(classname)
601 self._props = self._klass.getprops()
603 # do we prefix the form items with the item's identification?
604 self._anonymous = anonymous
606 HTMLInputMixin.__init__(self)
608 def __repr__(self):
609 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
610 self._nodeid)
612 def __getitem__(self, item):
613 ''' return an HTMLProperty instance
614 '''
615 #print 'HTMLItem.getitem', (self, item)
616 if item == 'id':
617 return self._nodeid
619 # get the property
620 prop = self._props[item]
622 # get the value, handling missing values
623 value = None
624 if int(self._nodeid) > 0:
625 value = self._klass.get(self._nodeid, item, None)
626 if value is None:
627 if isinstance(self._props[item], hyperdb.Multilink):
628 value = []
630 # look up the correct HTMLProperty class
631 for klass, htmlklass in propclasses:
632 if isinstance(prop, klass):
633 return htmlklass(self._client, self._classname,
634 self._nodeid, prop, item, value, self._anonymous)
636 raise KeyError, item
638 def __getattr__(self, attr):
639 ''' convenience access to properties '''
640 try:
641 return self[attr]
642 except KeyError:
643 raise AttributeError, attr
645 def designator(self):
646 """Return this item's designator (classname + id)."""
647 return '%s%s'%(self._classname, self._nodeid)
649 def is_retired(self):
650 """Is this item retired?"""
651 return self._klass.is_retired(self._nodeid)
653 def submit(self, label="Submit Changes"):
654 """Generate a submit button.
656 Also sneak in the lastactivity and action hidden elements.
657 """
658 return self.input(type="hidden", name="@lastactivity", value=date.Date('.')) + '\n' + \
659 self.input(type="hidden", name="@action", value="edit") + '\n' + \
660 self.input(type="submit", name="submit", value=label)
662 def journal(self, direction='descending'):
663 ''' Return a list of HTMLJournalEntry instances.
664 '''
665 # XXX do this
666 return []
668 def history(self, direction='descending', dre=re.compile('\d+')):
669 self.view_check()
671 l = ['<table class="history">'
672 '<tr><th colspan="4" class="header">',
673 _('History'),
674 '</th></tr><tr>',
675 _('<th>Date</th>'),
676 _('<th>User</th>'),
677 _('<th>Action</th>'),
678 _('<th>Args</th>'),
679 '</tr>']
680 current = {}
681 comments = {}
682 history = self._klass.history(self._nodeid)
683 history.sort()
684 timezone = self._db.getUserTimezone()
685 if direction == 'descending':
686 history.reverse()
687 for prop_n in self._props.keys():
688 prop = self[prop_n]
689 if isinstance(prop, HTMLProperty):
690 current[prop_n] = prop.plain()
691 # make link if hrefable
692 if (self._props.has_key(prop_n) and
693 isinstance(self._props[prop_n], hyperdb.Link)):
694 classname = self._props[prop_n].classname
695 try:
696 template = find_template(self._db.config.TEMPLATES,
697 classname, 'item')
698 if template[1].startswith('_generic'):
699 raise NoTemplate, 'not really...'
700 except NoTemplate:
701 pass
702 else:
703 id = self._klass.get(self._nodeid, prop_n, None)
704 current[prop_n] = '<a href="%s%s">%s</a>'%(
705 classname, id, current[prop_n])
707 for id, evt_date, user, action, args in history:
708 date_s = str(evt_date.local(timezone)).replace("."," ")
709 arg_s = ''
710 if action == 'link' and type(args) == type(()):
711 if len(args) == 3:
712 linkcl, linkid, key = args
713 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
714 linkcl, linkid, key)
715 else:
716 arg_s = str(args)
718 elif action == 'unlink' and type(args) == type(()):
719 if len(args) == 3:
720 linkcl, linkid, key = args
721 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
722 linkcl, linkid, key)
723 else:
724 arg_s = str(args)
726 elif type(args) == type({}):
727 cell = []
728 for k in args.keys():
729 # try to get the relevant property and treat it
730 # specially
731 try:
732 prop = self._props[k]
733 except KeyError:
734 prop = None
735 if prop is None:
736 # property no longer exists
737 comments['no_exist'] = _('''<em>The indicated property
738 no longer exists</em>''')
739 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
740 continue
742 if args[k] and (isinstance(prop, hyperdb.Multilink) or
743 isinstance(prop, hyperdb.Link)):
744 # figure what the link class is
745 classname = prop.classname
746 try:
747 linkcl = self._db.getclass(classname)
748 except KeyError:
749 labelprop = None
750 comments[classname] = _('''The linked class
751 %(classname)s no longer exists''')%locals()
752 labelprop = linkcl.labelprop(1)
753 try:
754 template = find_template(self._db.config.TEMPLATES,
755 classname, 'item')
756 if template[1].startswith('_generic'):
757 raise NoTemplate, 'not really...'
758 hrefable = 1
759 except NoTemplate:
760 hrefable = 0
762 if isinstance(prop, hyperdb.Multilink) and args[k]:
763 ml = []
764 for linkid in args[k]:
765 if isinstance(linkid, type(())):
766 sublabel = linkid[0] + ' '
767 linkids = linkid[1]
768 else:
769 sublabel = ''
770 linkids = [linkid]
771 subml = []
772 for linkid in linkids:
773 label = classname + linkid
774 # if we have a label property, try to use it
775 # TODO: test for node existence even when
776 # there's no labelprop!
777 try:
778 if labelprop is not None and \
779 labelprop != 'id':
780 label = linkcl.get(linkid, labelprop)
781 except IndexError:
782 comments['no_link'] = _('''<strike>The
783 linked node no longer
784 exists</strike>''')
785 subml.append('<strike>%s</strike>'%label)
786 else:
787 if hrefable:
788 subml.append('<a href="%s%s">%s</a>'%(
789 classname, linkid, label))
790 else:
791 subml.append(label)
792 ml.append(sublabel + ', '.join(subml))
793 cell.append('%s:\n %s'%(k, ', '.join(ml)))
794 elif isinstance(prop, hyperdb.Link) and args[k]:
795 label = classname + args[k]
796 # if we have a label property, try to use it
797 # TODO: test for node existence even when
798 # there's no labelprop!
799 if labelprop is not None and labelprop != 'id':
800 try:
801 label = linkcl.get(args[k], labelprop)
802 except IndexError:
803 comments['no_link'] = _('''<strike>The
804 linked node no longer
805 exists</strike>''')
806 cell.append(' <strike>%s</strike>,\n'%label)
807 # "flag" this is done .... euwww
808 label = None
809 if label is not None:
810 if hrefable:
811 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
812 else:
813 old = label;
814 cell.append('%s: %s' % (k,old))
815 if current.has_key(k):
816 cell[-1] += ' -> %s'%current[k]
817 current[k] = old
819 elif isinstance(prop, hyperdb.Date) and args[k]:
820 d = date.Date(args[k]).local(timezone)
821 cell.append('%s: %s'%(k, str(d)))
822 if current.has_key(k):
823 cell[-1] += ' -> %s' % current[k]
824 current[k] = str(d)
826 elif isinstance(prop, hyperdb.Interval) and args[k]:
827 d = date.Interval(args[k])
828 cell.append('%s: %s'%(k, str(d)))
829 if current.has_key(k):
830 cell[-1] += ' -> %s'%current[k]
831 current[k] = str(d)
833 elif isinstance(prop, hyperdb.String) and args[k]:
834 cell.append('%s: %s'%(k, cgi.escape(args[k])))
835 if current.has_key(k):
836 cell[-1] += ' -> %s'%current[k]
837 current[k] = cgi.escape(args[k])
839 elif not args[k]:
840 if current.has_key(k):
841 cell.append('%s: %s'%(k, current[k]))
842 current[k] = '(no value)'
843 else:
844 cell.append('%s: (no value)'%k)
846 else:
847 cell.append('%s: %s'%(k, str(args[k])))
848 if current.has_key(k):
849 cell[-1] += ' -> %s'%current[k]
850 current[k] = str(args[k])
852 arg_s = '<br />'.join(cell)
853 else:
854 # unkown event!!
855 comments['unknown'] = _('''<strong><em>This event is not
856 handled by the history display!</em></strong>''')
857 arg_s = '<strong><em>' + str(args) + '</em></strong>'
858 date_s = date_s.replace(' ', ' ')
859 # if the user's an itemid, figure the username (older journals
860 # have the username)
861 if dre.match(user):
862 user = self._db.user.get(user, 'username')
863 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
864 date_s, user, action, arg_s))
865 if comments:
866 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
867 for entry in comments.values():
868 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
869 l.append('</table>')
870 return '\n'.join(l)
872 def renderQueryForm(self):
873 ''' Render this item, which is a query, as a search form.
874 '''
875 # create a new request and override the specified args
876 req = HTMLRequest(self._client)
877 req.classname = self._klass.get(self._nodeid, 'klass')
878 name = self._klass.get(self._nodeid, 'name')
879 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
880 '&@queryname=%s'%urllib.quote(name))
882 # new template, using the specified classname and request
883 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
885 # use our fabricated request
886 return pt.render(self._client, req.classname, req)
888 class HTMLUserPermission:
890 def is_edit_ok(self):
891 ''' Is the user allowed to Edit the current class?
892 Also check whether this is the current user's info.
893 '''
894 return self._user_perm_check('Edit')
896 def is_view_ok(self):
897 ''' Is the user allowed to View the current class?
898 Also check whether this is the current user's info.
899 '''
900 return self._user_perm_check('View')
902 def _user_perm_check(self, type):
903 # some users may view / edit all users
904 s = self._db.security
905 userid = self._client.userid
906 if s.hasPermission(type, userid, self._classname):
907 return 1
909 # users may view their own info
910 is_anonymous = self._db.user.get(userid, 'username') == 'anonymous'
911 if getattr(self, '_nodeid', None) == userid and not is_anonymous:
912 return 1
914 # may anonymous users register?
915 if (is_anonymous and s.hasPermission('Web Registration', userid,
916 self._classname)):
917 return 1
919 # nope, no access here
920 return 0
922 class HTMLUserClass(HTMLUserPermission, HTMLClass):
923 pass
925 class HTMLUser(HTMLUserPermission, HTMLItem):
926 ''' Accesses through the *user* (a special case of item)
927 '''
928 def __init__(self, client, classname, nodeid, anonymous=0):
929 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
930 self._default_classname = client.classname
932 # used for security checks
933 self._security = client.db.security
935 _marker = []
936 def hasPermission(self, permission, classname=_marker):
937 ''' Determine if the user has the Permission.
939 The class being tested defaults to the template's class, but may
940 be overidden for this test by suppling an alternate classname.
941 '''
942 if classname is self._marker:
943 classname = self._default_classname
944 return self._security.hasPermission(permission, self._nodeid, classname)
946 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
947 ''' String, Number, Date, Interval HTMLProperty
949 Has useful attributes:
951 _name the name of the property
952 _value the value of the property if any
954 A wrapper object which may be stringified for the plain() behaviour.
955 '''
956 def __init__(self, client, classname, nodeid, prop, name, value,
957 anonymous=0):
958 self._client = client
959 self._db = client.db
960 self._classname = classname
961 self._nodeid = nodeid
962 self._prop = prop
963 self._value = value
964 self._anonymous = anonymous
965 self._name = name
966 if not anonymous:
967 self._formname = '%s%s@%s'%(classname, nodeid, name)
968 else:
969 self._formname = name
971 HTMLInputMixin.__init__(self)
973 def __repr__(self):
974 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
975 self._prop, self._value)
976 def __str__(self):
977 return self.plain()
978 def __cmp__(self, other):
979 if isinstance(other, HTMLProperty):
980 return cmp(self._value, other._value)
981 return cmp(self._value, other)
983 def is_edit_ok(self):
984 ''' Is the user allowed to Edit the current class?
985 '''
986 thing = HTMLDatabase(self._client)[self._classname]
987 if self._nodeid:
988 # this is a special-case for the User class where permission's
989 # on a per-item basis :(
990 thing = thing.getItem(self._nodeid)
991 return thing.is_edit_ok()
993 def is_view_ok(self):
994 ''' Is the user allowed to View the current class?
995 '''
996 thing = HTMLDatabase(self._client)[self._classname]
997 if self._nodeid:
998 # this is a special-case for the User class where permission's
999 # on a per-item basis :(
1000 thing = thing.getItem(self._nodeid)
1001 return thing.is_view_ok()
1003 class StringHTMLProperty(HTMLProperty):
1004 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
1005 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
1006 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
1007 def _hyper_repl(self, match):
1008 if match.group('url'):
1009 s = match.group('url')
1010 return '<a href="%s">%s</a>'%(s, s)
1011 elif match.group('email'):
1012 s = match.group('email')
1013 return '<a href="mailto:%s">%s</a>'%(s, s)
1014 else:
1015 s = match.group('item')
1016 s1 = match.group('class')
1017 s2 = match.group('id')
1018 try:
1019 # make sure s1 is a valid tracker classname
1020 cl = self._db.getclass(s1)
1021 if not cl.hasnode(s2):
1022 raise KeyError, 'oops'
1023 return '<a href="%s">%s%s</a>'%(s, s1, s2)
1024 except KeyError:
1025 return '%s%s'%(s1, s2)
1027 def hyperlinked(self):
1028 ''' Render a "hyperlinked" version of the text '''
1029 return self.plain(hyperlink=1)
1031 def plain(self, escape=0, hyperlink=0):
1032 '''Render a "plain" representation of the property
1034 - "escape" turns on/off HTML quoting
1035 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1036 addresses and designators
1037 '''
1038 self.view_check()
1040 if self._value is None:
1041 return ''
1042 if escape:
1043 s = cgi.escape(str(self._value))
1044 else:
1045 s = str(self._value)
1046 if hyperlink:
1047 # no, we *must* escape this text
1048 if not escape:
1049 s = cgi.escape(s)
1050 s = self.hyper_re.sub(self._hyper_repl, s)
1051 return s
1053 def stext(self, escape=0):
1054 ''' Render the value of the property as StructuredText.
1056 This requires the StructureText module to be installed separately.
1057 '''
1058 self.view_check()
1060 s = self.plain(escape=escape)
1061 if not StructuredText:
1062 return s
1063 return StructuredText(s,level=1,header=0)
1065 def field(self, size = 30):
1066 ''' Render the property as a field in HTML.
1068 If not editable, just display the value via plain().
1069 '''
1070 self.view_check()
1072 if self._value is None:
1073 value = ''
1074 else:
1075 value = cgi.escape(str(self._value))
1077 if self.is_edit_ok():
1078 value = '"'.join(value.split('"'))
1079 return self.input(name=self._formname,value=value,size=size)
1081 return self.plain()
1083 def multiline(self, escape=0, rows=5, cols=40):
1084 ''' Render a multiline form edit field for the property.
1086 If not editable, just display the plain() value in a <pre> tag.
1087 '''
1088 self.view_check()
1090 if self._value is None:
1091 value = ''
1092 else:
1093 value = cgi.escape(str(self._value))
1095 if self.is_edit_ok():
1096 value = '"'.join(value.split('"'))
1097 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1098 self._formname, rows, cols, value)
1100 return '<pre>%s</pre>'%self.plain()
1102 def email(self, escape=1):
1103 ''' Render the value of the property as an obscured email address
1104 '''
1105 self.view_check()
1107 if self._value is None:
1108 value = ''
1109 else:
1110 value = str(self._value)
1111 if value.find('@') != -1:
1112 name, domain = value.split('@')
1113 domain = ' '.join(domain.split('.')[:-1])
1114 name = name.replace('.', ' ')
1115 value = '%s at %s ...'%(name, domain)
1116 else:
1117 value = value.replace('.', ' ')
1118 if escape:
1119 value = cgi.escape(value)
1120 return value
1122 class PasswordHTMLProperty(HTMLProperty):
1123 def plain(self):
1124 ''' Render a "plain" representation of the property
1125 '''
1126 self.view_check()
1128 if self._value is None:
1129 return ''
1130 return _('*encrypted*')
1132 def field(self, size = 30):
1133 ''' Render a form edit field for the property.
1135 If not editable, just display the value via plain().
1136 '''
1137 self.view_check()
1139 if self.is_edit_ok():
1140 return self.input(type="password", name=self._formname, size=size)
1142 return self.plain()
1144 def confirm(self, size = 30):
1145 ''' Render a second form edit field for the property, used for
1146 confirmation that the user typed the password correctly. Generates
1147 a field with name "@confirm@name".
1149 If not editable, display nothing.
1150 '''
1151 self.view_check()
1153 if self.is_edit_ok():
1154 return self.input(type="password",
1155 name="@confirm@%s"%self._formname, size=size)
1157 return ''
1159 class NumberHTMLProperty(HTMLProperty):
1160 def plain(self):
1161 ''' Render a "plain" representation of the property
1162 '''
1163 self.view_check()
1165 return str(self._value)
1167 def field(self, size = 30):
1168 ''' Render a form edit field for the property.
1170 If not editable, just display the value via plain().
1171 '''
1172 self.view_check()
1174 if self._value is None:
1175 value = ''
1176 else:
1177 value = cgi.escape(str(self._value))
1179 if self.is_edit_ok():
1180 value = '"'.join(value.split('"'))
1181 return self.input(name=self._formname,value=value,size=size)
1183 return self.plain()
1185 def __int__(self):
1186 ''' Return an int of me
1187 '''
1188 return int(self._value)
1190 def __float__(self):
1191 ''' Return a float of me
1192 '''
1193 return float(self._value)
1196 class BooleanHTMLProperty(HTMLProperty):
1197 def plain(self):
1198 ''' Render a "plain" representation of the property
1199 '''
1200 self.view_check()
1202 if self._value is None:
1203 return ''
1204 return self._value and "Yes" or "No"
1206 def field(self):
1207 ''' Render a form edit field for the property
1209 If not editable, just display the value via plain().
1210 '''
1211 self.view_check()
1213 if not self.is_edit_ok():
1214 return self.plain()
1216 checked = self._value and "checked" or ""
1217 if self._value:
1218 s = self.input(type="radio", name=self._formname, value="yes",
1219 checked="checked")
1220 s += 'Yes'
1221 s +=self.input(type="radio", name=self._formname, value="no")
1222 s += 'No'
1223 else:
1224 s = self.input(type="radio", name=self._formname, value="yes")
1225 s += 'Yes'
1226 s +=self.input(type="radio", name=self._formname, value="no",
1227 checked="checked")
1228 s += 'No'
1229 return s
1231 class DateHTMLProperty(HTMLProperty):
1232 def plain(self):
1233 ''' Render a "plain" representation of the property
1234 '''
1235 self.view_check()
1237 if self._value is None:
1238 return ''
1239 return str(self._value.local(self._db.getUserTimezone()))
1241 def now(self):
1242 ''' Return the current time.
1244 This is useful for defaulting a new value. Returns a
1245 DateHTMLProperty.
1246 '''
1247 self.view_check()
1249 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1250 self._prop, self._formname, date.Date('.'))
1252 def field(self, size = 30):
1253 ''' Render a form edit field for the property
1255 If not editable, just display the value via plain().
1256 '''
1257 self.view_check()
1259 if self._value is None:
1260 value = ''
1261 else:
1262 tz = self._db.getUserTimezone()
1263 value = cgi.escape(str(self._value.local(tz)))
1265 if self.is_edit_ok():
1266 value = '"'.join(value.split('"'))
1267 return self.input(name=self._formname,value=value,size=size)
1269 return self.plain()
1271 def reldate(self, pretty=1):
1272 ''' Render the interval between the date and now.
1274 If the "pretty" flag is true, then make the display pretty.
1275 '''
1276 self.view_check()
1278 if not self._value:
1279 return ''
1281 # figure the interval
1282 interval = self._value - date.Date('.')
1283 if pretty:
1284 return interval.pretty()
1285 return str(interval)
1287 _marker = []
1288 def pretty(self, format=_marker):
1289 ''' Render the date in a pretty format (eg. month names, spaces).
1291 The format string is a standard python strftime format string.
1292 Note that if the day is zero, and appears at the start of the
1293 string, then it'll be stripped from the output. This is handy
1294 for the situatin when a date only specifies a month and a year.
1295 '''
1296 self.view_check()
1298 if format is not self._marker:
1299 return self._value.pretty(format)
1300 else:
1301 return self._value.pretty()
1303 def local(self, offset):
1304 ''' Return the date/time as a local (timezone offset) date/time.
1305 '''
1306 self.view_check()
1308 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1309 self._prop, self._formname, self._value.local(offset))
1311 class IntervalHTMLProperty(HTMLProperty):
1312 def plain(self):
1313 ''' Render a "plain" representation of the property
1314 '''
1315 self.view_check()
1317 if self._value is None:
1318 return ''
1319 return str(self._value)
1321 def pretty(self):
1322 ''' Render the interval in a pretty format (eg. "yesterday")
1323 '''
1324 self.view_check()
1326 return self._value.pretty()
1328 def field(self, size = 30):
1329 ''' Render a form edit field for the property
1331 If not editable, just display the value via plain().
1332 '''
1333 self.view_check()
1335 if self._value is None:
1336 value = ''
1337 else:
1338 value = cgi.escape(str(self._value))
1340 if is_edit_ok():
1341 value = '"'.join(value.split('"'))
1342 return self.input(name=self._formname,value=value,size=size)
1344 return self.plain()
1346 class LinkHTMLProperty(HTMLProperty):
1347 ''' Link HTMLProperty
1348 Include the above as well as being able to access the class
1349 information. Stringifying the object itself results in the value
1350 from the item being displayed. Accessing attributes of this object
1351 result in the appropriate entry from the class being queried for the
1352 property accessed (so item/assignedto/name would look up the user
1353 entry identified by the assignedto property on item, and then the
1354 name property of that user)
1355 '''
1356 def __init__(self, *args, **kw):
1357 HTMLProperty.__init__(self, *args, **kw)
1358 # if we're representing a form value, then the -1 from the form really
1359 # should be a None
1360 if str(self._value) == '-1':
1361 self._value = None
1363 def __getattr__(self, attr):
1364 ''' return a new HTMLItem '''
1365 #print 'Link.getattr', (self, attr, self._value)
1366 if not self._value:
1367 raise AttributeError, "Can't access missing value"
1368 if self._prop.classname == 'user':
1369 klass = HTMLUser
1370 else:
1371 klass = HTMLItem
1372 i = klass(self._client, self._prop.classname, self._value)
1373 return getattr(i, attr)
1375 def plain(self, escape=0):
1376 ''' Render a "plain" representation of the property
1377 '''
1378 self.view_check()
1380 if self._value is None:
1381 return ''
1382 linkcl = self._db.classes[self._prop.classname]
1383 k = linkcl.labelprop(1)
1384 value = str(linkcl.get(self._value, k))
1385 if escape:
1386 value = cgi.escape(value)
1387 return value
1389 def field(self, showid=0, size=None):
1390 ''' Render a form edit field for the property
1392 If not editable, just display the value via plain().
1393 '''
1394 self.view_check()
1396 if not self.is_edit_ok():
1397 return self.plain()
1399 # edit field
1400 linkcl = self._db.getclass(self._prop.classname)
1401 if self._value is None:
1402 value = ''
1403 else:
1404 k = linkcl.getkey()
1405 if k:
1406 value = linkcl.get(self._value, k)
1407 else:
1408 value = self._value
1409 value = cgi.escape(str(value))
1410 value = '"'.join(value.split('"'))
1411 return '<input name="%s" value="%s" size="%s">'%(self._formname,
1412 value, size)
1414 def menu(self, size=None, height=None, showid=0, additional=[],
1415 sort_on=None, **conditions):
1416 ''' Render a form select list for this property
1418 If not editable, just display the value via plain().
1419 '''
1420 self.view_check()
1422 if not self.is_edit_ok():
1423 return self.plain()
1425 value = self._value
1427 linkcl = self._db.getclass(self._prop.classname)
1428 l = ['<select name="%s">'%self._formname]
1429 k = linkcl.labelprop(1)
1430 s = ''
1431 if value is None:
1432 s = 'selected="selected" '
1433 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1434 if linkcl.getprops().has_key('order'):
1435 sort_on = ('+', 'order')
1436 else:
1437 if sort_on is None:
1438 sort_on = ('+', linkcl.labelprop())
1439 else:
1440 sort_on = ('+', sort_on)
1441 options = linkcl.filter(None, conditions, sort_on, (None, None))
1443 # make sure we list the current value if it's retired
1444 if self._value and self._value not in options:
1445 options.insert(0, self._value)
1447 for optionid in options:
1448 # get the option value, and if it's None use an empty string
1449 option = linkcl.get(optionid, k) or ''
1451 # figure if this option is selected
1452 s = ''
1453 if value in [optionid, option]:
1454 s = 'selected="selected" '
1456 # figure the label
1457 if showid:
1458 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1459 else:
1460 lab = option
1462 # truncate if it's too long
1463 if size is not None and len(lab) > size:
1464 lab = lab[:size-3] + '...'
1465 if additional:
1466 m = []
1467 for propname in additional:
1468 m.append(linkcl.get(optionid, propname))
1469 lab = lab + ' (%s)'%', '.join(map(str, m))
1471 # and generate
1472 lab = cgi.escape(lab)
1473 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1474 l.append('</select>')
1475 return '\n'.join(l)
1476 # def checklist(self, ...)
1478 class MultilinkHTMLProperty(HTMLProperty):
1479 ''' Multilink HTMLProperty
1481 Also be iterable, returning a wrapper object like the Link case for
1482 each entry in the multilink.
1483 '''
1484 def __init__(self, *args, **kwargs):
1485 HTMLProperty.__init__(self, *args, **kwargs)
1486 if self._value:
1487 sortfun = make_sort_function(self._db, self._prop.classname)
1488 self._value.sort(sortfun)
1490 def __len__(self):
1491 ''' length of the multilink '''
1492 return len(self._value)
1494 def __getattr__(self, attr):
1495 ''' no extended attribute accesses make sense here '''
1496 raise AttributeError, attr
1498 def __getitem__(self, num):
1499 ''' iterate and return a new HTMLItem
1500 '''
1501 #print 'Multi.getitem', (self, num)
1502 value = self._value[num]
1503 if self._prop.classname == 'user':
1504 klass = HTMLUser
1505 else:
1506 klass = HTMLItem
1507 return klass(self._client, self._prop.classname, value)
1509 def __contains__(self, value):
1510 ''' Support the "in" operator. We have to make sure the passed-in
1511 value is a string first, not a HTMLProperty.
1512 '''
1513 print (self, value, self._value)
1514 return str(value) in self._value
1516 def reverse(self):
1517 ''' return the list in reverse order
1518 '''
1519 l = self._value[:]
1520 l.reverse()
1521 if self._prop.classname == 'user':
1522 klass = HTMLUser
1523 else:
1524 klass = HTMLItem
1525 return [klass(self._client, self._prop.classname, value) for value in l]
1527 def plain(self, escape=0):
1528 ''' Render a "plain" representation of the property
1529 '''
1530 self.view_check()
1532 linkcl = self._db.classes[self._prop.classname]
1533 k = linkcl.labelprop(1)
1534 labels = []
1535 for v in self._value:
1536 labels.append(linkcl.get(v, k))
1537 value = ', '.join(labels)
1538 if escape:
1539 value = cgi.escape(value)
1540 return value
1542 def field(self, size=30, showid=0):
1543 ''' Render a form edit field for the property
1545 If not editable, just display the value via plain().
1546 '''
1547 self.view_check()
1549 if not self.is_edit_ok():
1550 return self.plain()
1552 linkcl = self._db.getclass(self._prop.classname)
1553 value = self._value[:]
1554 # map the id to the label property
1555 if not linkcl.getkey():
1556 showid=1
1557 if not showid:
1558 k = linkcl.labelprop(1)
1559 value = lookupKeys(linkcl, k, value)
1560 value = cgi.escape(','.join(value))
1561 return self.input(name=self._formname,size=size,value=value)
1563 def menu(self, size=None, height=None, showid=0, additional=[],
1564 sort_on=None, **conditions):
1565 ''' Render a form select list for this property
1567 If not editable, just display the value via plain().
1568 '''
1569 self.view_check()
1571 if not self.is_edit_ok():
1572 return self.plain()
1574 value = self._value
1576 linkcl = self._db.getclass(self._prop.classname)
1577 if sort_on is None:
1578 sort_on = ('+', find_sort_key(linkcl))
1579 else:
1580 sort_on = ('+', sort_on)
1581 options = linkcl.filter(None, conditions, sort_on)
1582 height = height or min(len(options), 7)
1583 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1584 k = linkcl.labelprop(1)
1586 # make sure we list the current values if they're retired
1587 for val in value:
1588 if val not in options:
1589 options.insert(0, val)
1591 for optionid in options:
1592 # get the option value, and if it's None use an empty string
1593 option = linkcl.get(optionid, k) or ''
1595 # figure if this option is selected
1596 s = ''
1597 if optionid in value or option in value:
1598 s = 'selected="selected" '
1600 # figure the label
1601 if showid:
1602 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1603 else:
1604 lab = option
1605 # truncate if it's too long
1606 if size is not None and len(lab) > size:
1607 lab = lab[:size-3] + '...'
1608 if additional:
1609 m = []
1610 for propname in additional:
1611 m.append(linkcl.get(optionid, propname))
1612 lab = lab + ' (%s)'%', '.join(m)
1614 # and generate
1615 lab = cgi.escape(lab)
1616 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1617 lab))
1618 l.append('</select>')
1619 return '\n'.join(l)
1621 # set the propclasses for HTMLItem
1622 propclasses = (
1623 (hyperdb.String, StringHTMLProperty),
1624 (hyperdb.Number, NumberHTMLProperty),
1625 (hyperdb.Boolean, BooleanHTMLProperty),
1626 (hyperdb.Date, DateHTMLProperty),
1627 (hyperdb.Interval, IntervalHTMLProperty),
1628 (hyperdb.Password, PasswordHTMLProperty),
1629 (hyperdb.Link, LinkHTMLProperty),
1630 (hyperdb.Multilink, MultilinkHTMLProperty),
1631 )
1633 def make_sort_function(db, classname, sort_on=None):
1634 '''Make a sort function for a given class
1635 '''
1636 linkcl = db.getclass(classname)
1637 if sort_on is None:
1638 sort_on = find_sort_key(linkcl)
1639 def sortfunc(a, b):
1640 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1641 return sortfunc
1643 def find_sort_key(linkcl):
1644 if linkcl.getprops().has_key('order'):
1645 return 'order'
1646 else:
1647 return linkcl.labelprop()
1649 def handleListCGIValue(value):
1650 ''' Value is either a single item or a list of items. Each item has a
1651 .value that we're actually interested in.
1652 '''
1653 if isinstance(value, type([])):
1654 return [value.value for value in value]
1655 else:
1656 value = value.value.strip()
1657 if not value:
1658 return []
1659 return value.split(',')
1661 class ShowDict:
1662 ''' A convenience access to the :columns index parameters
1663 '''
1664 def __init__(self, columns):
1665 self.columns = {}
1666 for col in columns:
1667 self.columns[col] = 1
1668 def __getitem__(self, name):
1669 return self.columns.has_key(name)
1671 class HTMLRequest(HTMLInputMixin):
1672 '''The *request*, holding the CGI form and environment.
1674 - "form" the CGI form as a cgi.FieldStorage
1675 - "env" the CGI environment variables
1676 - "base" the base URL for this instance
1677 - "user" a HTMLUser instance for this user
1678 - "classname" the current classname (possibly None)
1679 - "template" the current template (suffix, also possibly None)
1681 Index args:
1683 - "columns" dictionary of the columns to display in an index page
1684 - "show" a convenience access to columns - request/show/colname will
1685 be true if the columns should be displayed, false otherwise
1686 - "sort" index sort column (direction, column name)
1687 - "group" index grouping property (direction, column name)
1688 - "filter" properties to filter the index on
1689 - "filterspec" values to filter the index on
1690 - "search_text" text to perform a full-text search on for an index
1691 '''
1692 def __init__(self, client):
1693 # _client is needed by HTMLInputMixin
1694 self._client = self.client = client
1696 # easier access vars
1697 self.form = client.form
1698 self.env = client.env
1699 self.base = client.base
1700 self.user = HTMLUser(client, 'user', client.userid)
1702 # store the current class name and action
1703 self.classname = client.classname
1704 self.template = client.template
1706 # the special char to use for special vars
1707 self.special_char = '@'
1709 HTMLInputMixin.__init__(self)
1711 self._post_init()
1713 def _post_init(self):
1714 ''' Set attributes based on self.form
1715 '''
1716 # extract the index display information from the form
1717 self.columns = []
1718 for name in ':columns @columns'.split():
1719 if self.form.has_key(name):
1720 self.special_char = name[0]
1721 self.columns = handleListCGIValue(self.form[name])
1722 break
1723 self.show = ShowDict(self.columns)
1725 # sorting
1726 self.sort = (None, None)
1727 for name in ':sort @sort'.split():
1728 if self.form.has_key(name):
1729 self.special_char = name[0]
1730 sort = self.form[name].value
1731 if sort.startswith('-'):
1732 self.sort = ('-', sort[1:])
1733 else:
1734 self.sort = ('+', sort)
1735 if self.form.has_key(self.special_char+'sortdir'):
1736 self.sort = ('-', self.sort[1])
1738 # grouping
1739 self.group = (None, None)
1740 for name in ':group @group'.split():
1741 if self.form.has_key(name):
1742 self.special_char = name[0]
1743 group = self.form[name].value
1744 if group.startswith('-'):
1745 self.group = ('-', group[1:])
1746 else:
1747 self.group = ('+', group)
1748 if self.form.has_key(self.special_char+'groupdir'):
1749 self.group = ('-', self.group[1])
1751 # filtering
1752 self.filter = []
1753 for name in ':filter @filter'.split():
1754 if self.form.has_key(name):
1755 self.special_char = name[0]
1756 self.filter = handleListCGIValue(self.form[name])
1758 self.filterspec = {}
1759 db = self.client.db
1760 if self.classname is not None:
1761 props = db.getclass(self.classname).getprops()
1762 for name in self.filter:
1763 if not self.form.has_key(name):
1764 continue
1765 prop = props[name]
1766 fv = self.form[name]
1767 if (isinstance(prop, hyperdb.Link) or
1768 isinstance(prop, hyperdb.Multilink)):
1769 self.filterspec[name] = lookupIds(db, prop,
1770 handleListCGIValue(fv))
1771 else:
1772 if isinstance(fv, type([])):
1773 self.filterspec[name] = [v.value for v in fv]
1774 else:
1775 self.filterspec[name] = fv.value
1777 # full-text search argument
1778 self.search_text = None
1779 for name in ':search_text @search_text'.split():
1780 if self.form.has_key(name):
1781 self.special_char = name[0]
1782 self.search_text = self.form[name].value
1784 # pagination - size and start index
1785 # figure batch args
1786 self.pagesize = 50
1787 for name in ':pagesize @pagesize'.split():
1788 if self.form.has_key(name):
1789 self.special_char = name[0]
1790 self.pagesize = int(self.form[name].value)
1792 self.startwith = 0
1793 for name in ':startwith @startwith'.split():
1794 if self.form.has_key(name):
1795 self.special_char = name[0]
1796 self.startwith = int(self.form[name].value)
1798 def updateFromURL(self, url):
1799 ''' Parse the URL for query args, and update my attributes using the
1800 values.
1801 '''
1802 env = {'QUERY_STRING': url}
1803 self.form = cgi.FieldStorage(environ=env)
1805 self._post_init()
1807 def update(self, kwargs):
1808 ''' Update my attributes using the keyword args
1809 '''
1810 self.__dict__.update(kwargs)
1811 if kwargs.has_key('columns'):
1812 self.show = ShowDict(self.columns)
1814 def description(self):
1815 ''' Return a description of the request - handle for the page title.
1816 '''
1817 s = [self.client.db.config.TRACKER_NAME]
1818 if self.classname:
1819 if self.client.nodeid:
1820 s.append('- %s%s'%(self.classname, self.client.nodeid))
1821 else:
1822 if self.template == 'item':
1823 s.append('- new %s'%self.classname)
1824 elif self.template == 'index':
1825 s.append('- %s index'%self.classname)
1826 else:
1827 s.append('- %s %s'%(self.classname, self.template))
1828 else:
1829 s.append('- home')
1830 return ' '.join(s)
1832 def __str__(self):
1833 d = {}
1834 d.update(self.__dict__)
1835 f = ''
1836 for k in self.form.keys():
1837 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1838 d['form'] = f
1839 e = ''
1840 for k,v in self.env.items():
1841 e += '\n %r=%r'%(k, v)
1842 d['env'] = e
1843 return '''
1844 form: %(form)s
1845 base: %(base)r
1846 classname: %(classname)r
1847 template: %(template)r
1848 columns: %(columns)r
1849 sort: %(sort)r
1850 group: %(group)r
1851 filter: %(filter)r
1852 search_text: %(search_text)r
1853 pagesize: %(pagesize)r
1854 startwith: %(startwith)r
1855 env: %(env)s
1856 '''%d
1858 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1859 filterspec=1):
1860 ''' return the current index args as form elements '''
1861 l = []
1862 sc = self.special_char
1863 s = self.input(type="hidden",name="%s",value="%s")
1864 if columns and self.columns:
1865 l.append(s%(sc+'columns', ','.join(self.columns)))
1866 if sort and self.sort[1] is not None:
1867 if self.sort[0] == '-':
1868 val = '-'+self.sort[1]
1869 else:
1870 val = self.sort[1]
1871 l.append(s%(sc+'sort', val))
1872 if group and self.group[1] is not None:
1873 if self.group[0] == '-':
1874 val = '-'+self.group[1]
1875 else:
1876 val = self.group[1]
1877 l.append(s%(sc+'group', val))
1878 if filter and self.filter:
1879 l.append(s%(sc+'filter', ','.join(self.filter)))
1880 if filterspec:
1881 for k,v in self.filterspec.items():
1882 if type(v) == type([]):
1883 l.append(s%(k, ','.join(v)))
1884 else:
1885 l.append(s%(k, v))
1886 if self.search_text:
1887 l.append(s%(sc+'search_text', self.search_text))
1888 l.append(s%(sc+'pagesize', self.pagesize))
1889 l.append(s%(sc+'startwith', self.startwith))
1890 return '\n'.join(l)
1892 def indexargs_url(self, url, args):
1893 ''' Embed the current index args in a URL
1894 '''
1895 sc = self.special_char
1896 l = ['%s=%s'%(k,v) for k,v in args.items()]
1898 # pull out the special values (prefixed by @ or :)
1899 specials = {}
1900 for key in args.keys():
1901 if key[0] in '@:':
1902 specials[key[1:]] = args[key]
1904 # ok, now handle the specials we received in the request
1905 if self.columns and not specials.has_key('columns'):
1906 l.append(sc+'columns=%s'%(','.join(self.columns)))
1907 if self.sort[1] is not None and not specials.has_key('sort'):
1908 if self.sort[0] == '-':
1909 val = '-'+self.sort[1]
1910 else:
1911 val = self.sort[1]
1912 l.append(sc+'sort=%s'%val)
1913 if self.group[1] is not None and not specials.has_key('group'):
1914 if self.group[0] == '-':
1915 val = '-'+self.group[1]
1916 else:
1917 val = self.group[1]
1918 l.append(sc+'group=%s'%val)
1919 if self.filter and not specials.has_key('filter'):
1920 l.append(sc+'filter=%s'%(','.join(self.filter)))
1921 if self.search_text and not specials.has_key('search_text'):
1922 l.append(sc+'search_text=%s'%self.search_text)
1923 if not specials.has_key('pagesize'):
1924 l.append(sc+'pagesize=%s'%self.pagesize)
1925 if not specials.has_key('startwith'):
1926 l.append(sc+'startwith=%s'%self.startwith)
1928 # finally, the remainder of the filter args in the request
1929 for k,v in self.filterspec.items():
1930 if not args.has_key(k):
1931 if type(v) == type([]):
1932 l.append('%s=%s'%(k, ','.join(v)))
1933 else:
1934 l.append('%s=%s'%(k, v))
1935 return '%s?%s'%(url, '&'.join(l))
1936 indexargs_href = indexargs_url
1938 def base_javascript(self):
1939 return '''
1940 <script type="text/javascript">
1941 submitted = false;
1942 function submit_once() {
1943 if (submitted) {
1944 alert("Your request is being processed.\\nPlease be patient.");
1945 event.returnValue = 0; // work-around for IE
1946 return 0;
1947 }
1948 submitted = true;
1949 return 1;
1950 }
1952 function help_window(helpurl, width, height) {
1953 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1954 }
1955 </script>
1956 '''%self.base
1958 def batch(self):
1959 ''' Return a batch object for results from the "current search"
1960 '''
1961 filterspec = self.filterspec
1962 sort = self.sort
1963 group = self.group
1965 # get the list of ids we're batching over
1966 klass = self.client.db.getclass(self.classname)
1967 if self.search_text:
1968 matches = self.client.db.indexer.search(
1969 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1970 else:
1971 matches = None
1972 l = klass.filter(matches, filterspec, sort, group)
1974 # return the batch object, using IDs only
1975 return Batch(self.client, l, self.pagesize, self.startwith,
1976 classname=self.classname)
1978 # extend the standard ZTUtils Batch object to remove dependency on
1979 # Acquisition and add a couple of useful methods
1980 class Batch(ZTUtils.Batch):
1981 ''' Use me to turn a list of items, or item ids of a given class, into a
1982 series of batches.
1984 ========= ========================================================
1985 Parameter Usage
1986 ========= ========================================================
1987 sequence a list of HTMLItems or item ids
1988 classname if sequence is a list of ids, this is the class of item
1989 size how big to make the sequence.
1990 start where to start (0-indexed) in the sequence.
1991 end where to end (0-indexed) in the sequence.
1992 orphan if the next batch would contain less items than this
1993 value, then it is combined with this batch
1994 overlap the number of items shared between adjacent batches
1995 ========= ========================================================
1997 Attributes: Note that the "start" attribute, unlike the
1998 argument, is a 1-based index (I know, lame). "first" is the
1999 0-based index. "length" is the actual number of elements in
2000 the batch.
2002 "sequence_length" is the length of the original, unbatched, sequence.
2003 '''
2004 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2005 overlap=0, classname=None):
2006 self.client = client
2007 self.last_index = self.last_item = None
2008 self.current_item = None
2009 self.classname = classname
2010 self.sequence_length = len(sequence)
2011 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2012 overlap)
2014 # overwrite so we can late-instantiate the HTMLItem instance
2015 def __getitem__(self, index):
2016 if index < 0:
2017 if index + self.end < self.first: raise IndexError, index
2018 return self._sequence[index + self.end]
2020 if index >= self.length:
2021 raise IndexError, index
2023 # move the last_item along - but only if the fetched index changes
2024 # (for some reason, index 0 is fetched twice)
2025 if index != self.last_index:
2026 self.last_item = self.current_item
2027 self.last_index = index
2029 item = self._sequence[index + self.first]
2030 if self.classname:
2031 # map the item ids to instances
2032 if self.classname == 'user':
2033 item = HTMLUser(self.client, self.classname, item)
2034 else:
2035 item = HTMLItem(self.client, self.classname, item)
2036 self.current_item = item
2037 return item
2039 def propchanged(self, property):
2040 ''' Detect if the property marked as being the group property
2041 changed in the last iteration fetch
2042 '''
2043 if (self.last_item is None or
2044 self.last_item[property] != self.current_item[property]):
2045 return 1
2046 return 0
2048 # override these 'cos we don't have access to acquisition
2049 def previous(self):
2050 if self.start == 1:
2051 return None
2052 return Batch(self.client, self._sequence, self._size,
2053 self.first - self._size + self.overlap, 0, self.orphan,
2054 self.overlap)
2056 def next(self):
2057 try:
2058 self._sequence[self.end]
2059 except IndexError:
2060 return None
2061 return Batch(self.client, self._sequence, self._size,
2062 self.end - self.overlap, 0, self.orphan, self.overlap)
2064 class TemplatingUtils:
2065 ''' Utilities for templating
2066 '''
2067 def __init__(self, client):
2068 self.client = client
2069 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2070 return Batch(self.client, sequence, size, start, end, orphan,
2071 overlap)