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 # pre-load the history with the current state
693 for prop_n in self._props.keys():
694 prop = self[prop_n]
695 if not isinstance(prop, HTMLProperty):
696 continue
697 current[prop_n] = prop.plain()
698 # make link if hrefable
699 if (self._props.has_key(prop_n) and
700 isinstance(self._props[prop_n], hyperdb.Link)):
701 classname = self._props[prop_n].classname
702 try:
703 template = find_template(self._db.config.TEMPLATES,
704 classname, 'item')
705 if template[1].startswith('_generic'):
706 raise NoTemplate, 'not really...'
707 except NoTemplate:
708 pass
709 else:
710 id = self._klass.get(self._nodeid, prop_n, None)
711 current[prop_n] = '<a href="%s%s">%s</a>'%(
712 classname, id, current[prop_n])
714 for id, evt_date, user, action, args in history:
715 date_s = str(evt_date.local(timezone)).replace("."," ")
716 arg_s = ''
717 if action == 'link' and type(args) == type(()):
718 if len(args) == 3:
719 linkcl, linkid, key = args
720 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
721 linkcl, linkid, key)
722 else:
723 arg_s = str(args)
725 elif action == 'unlink' and type(args) == type(()):
726 if len(args) == 3:
727 linkcl, linkid, key = args
728 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
729 linkcl, linkid, key)
730 else:
731 arg_s = str(args)
733 elif type(args) == type({}):
734 cell = []
735 for k in args.keys():
736 # try to get the relevant property and treat it
737 # specially
738 try:
739 prop = self._props[k]
740 except KeyError:
741 prop = None
742 if prop is None:
743 # property no longer exists
744 comments['no_exist'] = _('''<em>The indicated property
745 no longer exists</em>''')
746 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
747 continue
749 if args[k] and (isinstance(prop, hyperdb.Multilink) or
750 isinstance(prop, hyperdb.Link)):
751 # figure what the link class is
752 classname = prop.classname
753 try:
754 linkcl = self._db.getclass(classname)
755 except KeyError:
756 labelprop = None
757 comments[classname] = _('''The linked class
758 %(classname)s no longer exists''')%locals()
759 labelprop = linkcl.labelprop(1)
760 try:
761 template = find_template(self._db.config.TEMPLATES,
762 classname, 'item')
763 if template[1].startswith('_generic'):
764 raise NoTemplate, 'not really...'
765 hrefable = 1
766 except NoTemplate:
767 hrefable = 0
769 if isinstance(prop, hyperdb.Multilink) and args[k]:
770 ml = []
771 for linkid in args[k]:
772 if isinstance(linkid, type(())):
773 sublabel = linkid[0] + ' '
774 linkids = linkid[1]
775 else:
776 sublabel = ''
777 linkids = [linkid]
778 subml = []
779 for linkid in linkids:
780 label = classname + linkid
781 # if we have a label property, try to use it
782 # TODO: test for node existence even when
783 # there's no labelprop!
784 try:
785 if labelprop is not None and \
786 labelprop != 'id':
787 label = linkcl.get(linkid, labelprop)
788 except IndexError:
789 comments['no_link'] = _('''<strike>The
790 linked node no longer
791 exists</strike>''')
792 subml.append('<strike>%s</strike>'%label)
793 else:
794 if hrefable:
795 subml.append('<a href="%s%s">%s</a>'%(
796 classname, linkid, label))
797 else:
798 subml.append(label)
799 ml.append(sublabel + ', '.join(subml))
800 cell.append('%s:\n %s'%(k, ', '.join(ml)))
801 elif isinstance(prop, hyperdb.Link) and args[k]:
802 label = classname + args[k]
803 # if we have a label property, try to use it
804 # TODO: test for node existence even when
805 # there's no labelprop!
806 if labelprop is not None and labelprop != 'id':
807 try:
808 label = linkcl.get(args[k], labelprop)
809 except IndexError:
810 comments['no_link'] = _('''<strike>The
811 linked node no longer
812 exists</strike>''')
813 cell.append(' <strike>%s</strike>,\n'%label)
814 # "flag" this is done .... euwww
815 label = None
816 if label is not None:
817 if hrefable:
818 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
819 else:
820 old = label;
821 cell.append('%s: %s' % (k,old))
822 if current.has_key(k):
823 cell[-1] += ' -> %s'%current[k]
824 current[k] = old
826 elif isinstance(prop, hyperdb.Date) and args[k]:
827 d = date.Date(args[k]).local(timezone)
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.Interval) and args[k]:
834 val = str(date.Interval(args[k]))
835 cell.append('%s: %s'%(k, val))
836 if current.has_key(k):
837 cell[-1] += ' -> %s'%current[k]
838 current[k] = val
840 elif isinstance(prop, hyperdb.String) and args[k]:
841 val = cgi.escape(args[k])
842 cell.append('%s: %s'%(k, val))
843 if current.has_key(k):
844 cell[-1] += ' -> %s'%current[k]
845 current[k] = val
847 elif isinstance(prop, hyperdb.Boolean) and args[k] is not None:
848 val = args[k] and 'Yes' or 'No'
849 cell.append('%s: %s'%(k, val))
850 if current.has_key(k):
851 cell[-1] += ' -> %s'%current[k]
852 current[k] = val
854 elif not args[k]:
855 if current.has_key(k):
856 cell.append('%s: %s'%(k, current[k]))
857 current[k] = '(no value)'
858 else:
859 cell.append('%s: (no value)'%k)
861 else:
862 cell.append('%s: %s'%(k, str(args[k])))
863 if current.has_key(k):
864 cell[-1] += ' -> %s'%current[k]
865 current[k] = str(args[k])
867 arg_s = '<br />'.join(cell)
868 else:
869 # unkown event!!
870 comments['unknown'] = _('''<strong><em>This event is not
871 handled by the history display!</em></strong>''')
872 arg_s = '<strong><em>' + str(args) + '</em></strong>'
873 date_s = date_s.replace(' ', ' ')
874 # if the user's an itemid, figure the username (older journals
875 # have the username)
876 if dre.match(user):
877 user = self._db.user.get(user, 'username')
878 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
879 date_s, user, action, arg_s))
880 if comments:
881 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
882 for entry in comments.values():
883 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
884 l.append('</table>')
885 return '\n'.join(l)
887 def renderQueryForm(self):
888 ''' Render this item, which is a query, as a search form.
889 '''
890 # create a new request and override the specified args
891 req = HTMLRequest(self._client)
892 req.classname = self._klass.get(self._nodeid, 'klass')
893 name = self._klass.get(self._nodeid, 'name')
894 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
895 '&@queryname=%s'%urllib.quote(name))
897 # new template, using the specified classname and request
898 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
900 # use our fabricated request
901 return pt.render(self._client, req.classname, req)
903 class HTMLUserPermission:
905 def is_edit_ok(self):
906 ''' Is the user allowed to Edit the current class?
907 Also check whether this is the current user's info.
908 '''
909 return self._user_perm_check('Edit')
911 def is_view_ok(self):
912 ''' Is the user allowed to View the current class?
913 Also check whether this is the current user's info.
914 '''
915 return self._user_perm_check('View')
917 def _user_perm_check(self, type):
918 # some users may view / edit all users
919 s = self._db.security
920 userid = self._client.userid
921 if s.hasPermission(type, userid, self._classname):
922 return 1
924 # users may view their own info
925 is_anonymous = self._db.user.get(userid, 'username') == 'anonymous'
926 if getattr(self, '_nodeid', None) == userid and not is_anonymous:
927 return 1
929 # may anonymous users register?
930 if (is_anonymous and s.hasPermission('Web Registration', userid,
931 self._classname)):
932 return 1
934 # nope, no access here
935 return 0
937 class HTMLUserClass(HTMLUserPermission, HTMLClass):
938 pass
940 class HTMLUser(HTMLUserPermission, HTMLItem):
941 ''' Accesses through the *user* (a special case of item)
942 '''
943 def __init__(self, client, classname, nodeid, anonymous=0):
944 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
945 self._default_classname = client.classname
947 # used for security checks
948 self._security = client.db.security
950 _marker = []
951 def hasPermission(self, permission, classname=_marker):
952 ''' Determine if the user has the Permission.
954 The class being tested defaults to the template's class, but may
955 be overidden for this test by suppling an alternate classname.
956 '''
957 if classname is self._marker:
958 classname = self._default_classname
959 return self._security.hasPermission(permission, self._nodeid, classname)
961 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
962 ''' String, Number, Date, Interval HTMLProperty
964 Has useful attributes:
966 _name the name of the property
967 _value the value of the property if any
969 A wrapper object which may be stringified for the plain() behaviour.
970 '''
971 def __init__(self, client, classname, nodeid, prop, name, value,
972 anonymous=0):
973 self._client = client
974 self._db = client.db
975 self._classname = classname
976 self._nodeid = nodeid
977 self._prop = prop
978 self._value = value
979 self._anonymous = anonymous
980 self._name = name
981 if not anonymous:
982 self._formname = '%s%s@%s'%(classname, nodeid, name)
983 else:
984 self._formname = name
986 HTMLInputMixin.__init__(self)
988 def __repr__(self):
989 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
990 self._prop, self._value)
991 def __str__(self):
992 return self.plain()
993 def __cmp__(self, other):
994 if isinstance(other, HTMLProperty):
995 return cmp(self._value, other._value)
996 return cmp(self._value, other)
998 def is_edit_ok(self):
999 ''' Is the user allowed to Edit 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_edit_ok()
1008 def is_view_ok(self):
1009 ''' Is the user allowed to View the current class?
1010 '''
1011 thing = HTMLDatabase(self._client)[self._classname]
1012 if self._nodeid:
1013 # this is a special-case for the User class where permission's
1014 # on a per-item basis :(
1015 thing = thing.getItem(self._nodeid)
1016 return thing.is_view_ok()
1018 class StringHTMLProperty(HTMLProperty):
1019 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
1020 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
1021 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
1022 def _hyper_repl(self, match):
1023 if match.group('url'):
1024 s = match.group('url')
1025 return '<a href="%s">%s</a>'%(s, s)
1026 elif match.group('email'):
1027 s = match.group('email')
1028 return '<a href="mailto:%s">%s</a>'%(s, s)
1029 else:
1030 s = match.group('item')
1031 s1 = match.group('class')
1032 s2 = match.group('id')
1033 try:
1034 # make sure s1 is a valid tracker classname
1035 cl = self._db.getclass(s1)
1036 if not cl.hasnode(s2):
1037 raise KeyError, 'oops'
1038 return '<a href="%s">%s%s</a>'%(s, s1, s2)
1039 except KeyError:
1040 return '%s%s'%(s1, s2)
1042 def hyperlinked(self):
1043 ''' Render a "hyperlinked" version of the text '''
1044 return self.plain(hyperlink=1)
1046 def plain(self, escape=0, hyperlink=0):
1047 '''Render a "plain" representation of the property
1049 - "escape" turns on/off HTML quoting
1050 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1051 addresses and designators
1052 '''
1053 self.view_check()
1055 if self._value is None:
1056 return ''
1057 if escape:
1058 s = cgi.escape(str(self._value))
1059 else:
1060 s = str(self._value)
1061 if hyperlink:
1062 # no, we *must* escape this text
1063 if not escape:
1064 s = cgi.escape(s)
1065 s = self.hyper_re.sub(self._hyper_repl, s)
1066 return s
1068 def stext(self, escape=0):
1069 ''' Render the value of the property as StructuredText.
1071 This requires the StructureText module to be installed separately.
1072 '''
1073 self.view_check()
1075 s = self.plain(escape=escape)
1076 if not StructuredText:
1077 return s
1078 return StructuredText(s,level=1,header=0)
1080 def field(self, size = 30):
1081 ''' Render the property as a field in HTML.
1083 If not editable, just display the value via plain().
1084 '''
1085 self.view_check()
1087 if self._value is None:
1088 value = ''
1089 else:
1090 value = cgi.escape(str(self._value))
1092 if self.is_edit_ok():
1093 value = '"'.join(value.split('"'))
1094 return self.input(name=self._formname,value=value,size=size)
1096 return self.plain()
1098 def multiline(self, escape=0, rows=5, cols=40):
1099 ''' Render a multiline form edit field for the property.
1101 If not editable, just display the plain() value in a <pre> tag.
1102 '''
1103 self.view_check()
1105 if self._value is None:
1106 value = ''
1107 else:
1108 value = cgi.escape(str(self._value))
1110 if self.is_edit_ok():
1111 value = '"'.join(value.split('"'))
1112 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1113 self._formname, rows, cols, value)
1115 return '<pre>%s</pre>'%self.plain()
1117 def email(self, escape=1):
1118 ''' Render the value of the property as an obscured email address
1119 '''
1120 self.view_check()
1122 if self._value is None:
1123 value = ''
1124 else:
1125 value = str(self._value)
1126 if value.find('@') != -1:
1127 name, domain = value.split('@')
1128 domain = ' '.join(domain.split('.')[:-1])
1129 name = name.replace('.', ' ')
1130 value = '%s at %s ...'%(name, domain)
1131 else:
1132 value = value.replace('.', ' ')
1133 if escape:
1134 value = cgi.escape(value)
1135 return value
1137 class PasswordHTMLProperty(HTMLProperty):
1138 def plain(self):
1139 ''' Render a "plain" representation of the property
1140 '''
1141 self.view_check()
1143 if self._value is None:
1144 return ''
1145 return _('*encrypted*')
1147 def field(self, size = 30):
1148 ''' Render a form edit field for the property.
1150 If not editable, just display the value via plain().
1151 '''
1152 self.view_check()
1154 if self.is_edit_ok():
1155 return self.input(type="password", name=self._formname, size=size)
1157 return self.plain()
1159 def confirm(self, size = 30):
1160 ''' Render a second form edit field for the property, used for
1161 confirmation that the user typed the password correctly. Generates
1162 a field with name "@confirm@name".
1164 If not editable, display nothing.
1165 '''
1166 self.view_check()
1168 if self.is_edit_ok():
1169 return self.input(type="password",
1170 name="@confirm@%s"%self._formname, size=size)
1172 return ''
1174 class NumberHTMLProperty(HTMLProperty):
1175 def plain(self):
1176 ''' Render a "plain" representation of the property
1177 '''
1178 self.view_check()
1180 return str(self._value)
1182 def field(self, size = 30):
1183 ''' Render a form edit field for the property.
1185 If not editable, just display the value via plain().
1186 '''
1187 self.view_check()
1189 if self._value is None:
1190 value = ''
1191 else:
1192 value = cgi.escape(str(self._value))
1194 if self.is_edit_ok():
1195 value = '"'.join(value.split('"'))
1196 return self.input(name=self._formname,value=value,size=size)
1198 return self.plain()
1200 def __int__(self):
1201 ''' Return an int of me
1202 '''
1203 return int(self._value)
1205 def __float__(self):
1206 ''' Return a float of me
1207 '''
1208 return float(self._value)
1211 class BooleanHTMLProperty(HTMLProperty):
1212 def plain(self):
1213 ''' Render a "plain" representation of the property
1214 '''
1215 self.view_check()
1217 if self._value is None:
1218 return ''
1219 return self._value and "Yes" or "No"
1221 def field(self):
1222 ''' Render a form edit field for the property
1224 If not editable, just display the value via plain().
1225 '''
1226 self.view_check()
1228 if not self.is_edit_ok():
1229 return self.plain()
1231 checked = self._value and "checked" or ""
1232 if self._value:
1233 s = self.input(type="radio", name=self._formname, value="yes",
1234 checked="checked")
1235 s += 'Yes'
1236 s +=self.input(type="radio", name=self._formname, value="no")
1237 s += 'No'
1238 else:
1239 s = self.input(type="radio", name=self._formname, value="yes")
1240 s += 'Yes'
1241 s +=self.input(type="radio", name=self._formname, value="no",
1242 checked="checked")
1243 s += 'No'
1244 return s
1246 class DateHTMLProperty(HTMLProperty):
1247 def plain(self):
1248 ''' Render a "plain" representation of the property
1249 '''
1250 self.view_check()
1252 if self._value is None:
1253 return ''
1254 return str(self._value.local(self._db.getUserTimezone()))
1256 def now(self):
1257 ''' Return the current time.
1259 This is useful for defaulting a new value. Returns a
1260 DateHTMLProperty.
1261 '''
1262 self.view_check()
1264 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1265 self._prop, self._formname, date.Date('.'))
1267 def field(self, size = 30):
1268 ''' Render a form edit field for the property
1270 If not editable, just display the value via plain().
1271 '''
1272 self.view_check()
1274 if self._value is None:
1275 value = ''
1276 else:
1277 tz = self._db.getUserTimezone()
1278 value = cgi.escape(str(self._value.local(tz)))
1280 if self.is_edit_ok():
1281 value = '"'.join(value.split('"'))
1282 return self.input(name=self._formname,value=value,size=size)
1284 return self.plain()
1286 def reldate(self, pretty=1):
1287 ''' Render the interval between the date and now.
1289 If the "pretty" flag is true, then make the display pretty.
1290 '''
1291 self.view_check()
1293 if not self._value:
1294 return ''
1296 # figure the interval
1297 interval = self._value - date.Date('.')
1298 if pretty:
1299 return interval.pretty()
1300 return str(interval)
1302 _marker = []
1303 def pretty(self, format=_marker):
1304 ''' Render the date in a pretty format (eg. month names, spaces).
1306 The format string is a standard python strftime format string.
1307 Note that if the day is zero, and appears at the start of the
1308 string, then it'll be stripped from the output. This is handy
1309 for the situatin when a date only specifies a month and a year.
1310 '''
1311 self.view_check()
1313 if format is not self._marker:
1314 return self._value.pretty(format)
1315 else:
1316 return self._value.pretty()
1318 def local(self, offset):
1319 ''' Return the date/time as a local (timezone offset) date/time.
1320 '''
1321 self.view_check()
1323 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1324 self._prop, self._formname, self._value.local(offset))
1326 class IntervalHTMLProperty(HTMLProperty):
1327 def plain(self):
1328 ''' Render a "plain" representation of the property
1329 '''
1330 self.view_check()
1332 if self._value is None:
1333 return ''
1334 return str(self._value)
1336 def pretty(self):
1337 ''' Render the interval in a pretty format (eg. "yesterday")
1338 '''
1339 self.view_check()
1341 return self._value.pretty()
1343 def field(self, size = 30):
1344 ''' Render a form edit field for the property
1346 If not editable, just display the value via plain().
1347 '''
1348 self.view_check()
1350 if self._value is None:
1351 value = ''
1352 else:
1353 value = cgi.escape(str(self._value))
1355 if is_edit_ok():
1356 value = '"'.join(value.split('"'))
1357 return self.input(name=self._formname,value=value,size=size)
1359 return self.plain()
1361 class LinkHTMLProperty(HTMLProperty):
1362 ''' Link HTMLProperty
1363 Include the above as well as being able to access the class
1364 information. Stringifying the object itself results in the value
1365 from the item being displayed. Accessing attributes of this object
1366 result in the appropriate entry from the class being queried for the
1367 property accessed (so item/assignedto/name would look up the user
1368 entry identified by the assignedto property on item, and then the
1369 name property of that user)
1370 '''
1371 def __init__(self, *args, **kw):
1372 HTMLProperty.__init__(self, *args, **kw)
1373 # if we're representing a form value, then the -1 from the form really
1374 # should be a None
1375 if str(self._value) == '-1':
1376 self._value = None
1378 def __getattr__(self, attr):
1379 ''' return a new HTMLItem '''
1380 #print 'Link.getattr', (self, attr, self._value)
1381 if not self._value:
1382 raise AttributeError, "Can't access missing value"
1383 if self._prop.classname == 'user':
1384 klass = HTMLUser
1385 else:
1386 klass = HTMLItem
1387 i = klass(self._client, self._prop.classname, self._value)
1388 return getattr(i, attr)
1390 def plain(self, escape=0):
1391 ''' Render a "plain" representation of the property
1392 '''
1393 self.view_check()
1395 if self._value is None:
1396 return ''
1397 linkcl = self._db.classes[self._prop.classname]
1398 k = linkcl.labelprop(1)
1399 value = str(linkcl.get(self._value, k))
1400 if escape:
1401 value = cgi.escape(value)
1402 return value
1404 def field(self, showid=0, size=None):
1405 ''' Render a form edit field for the property
1407 If not editable, just display the value via plain().
1408 '''
1409 self.view_check()
1411 if not self.is_edit_ok():
1412 return self.plain()
1414 # edit field
1415 linkcl = self._db.getclass(self._prop.classname)
1416 if self._value is None:
1417 value = ''
1418 else:
1419 k = linkcl.getkey()
1420 if k:
1421 value = linkcl.get(self._value, k)
1422 else:
1423 value = self._value
1424 value = cgi.escape(str(value))
1425 value = '"'.join(value.split('"'))
1426 return '<input name="%s" value="%s" size="%s">'%(self._formname,
1427 value, size)
1429 def menu(self, size=None, height=None, showid=0, additional=[],
1430 sort_on=None, **conditions):
1431 ''' Render a form select list for this property
1433 If not editable, just display the value via plain().
1434 '''
1435 self.view_check()
1437 if not self.is_edit_ok():
1438 return self.plain()
1440 value = self._value
1442 linkcl = self._db.getclass(self._prop.classname)
1443 l = ['<select name="%s">'%self._formname]
1444 k = linkcl.labelprop(1)
1445 s = ''
1446 if value is None:
1447 s = 'selected="selected" '
1448 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1449 if linkcl.getprops().has_key('order'):
1450 sort_on = ('+', 'order')
1451 else:
1452 if sort_on is None:
1453 sort_on = ('+', linkcl.labelprop())
1454 else:
1455 sort_on = ('+', sort_on)
1456 options = linkcl.filter(None, conditions, sort_on, (None, None))
1458 # make sure we list the current value if it's retired
1459 if self._value and self._value not in options:
1460 options.insert(0, self._value)
1462 for optionid in options:
1463 # get the option value, and if it's None use an empty string
1464 option = linkcl.get(optionid, k) or ''
1466 # figure if this option is selected
1467 s = ''
1468 if value in [optionid, option]:
1469 s = 'selected="selected" '
1471 # figure the label
1472 if showid:
1473 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1474 else:
1475 lab = option
1477 # truncate if it's too long
1478 if size is not None and len(lab) > size:
1479 lab = lab[:size-3] + '...'
1480 if additional:
1481 m = []
1482 for propname in additional:
1483 m.append(linkcl.get(optionid, propname))
1484 lab = lab + ' (%s)'%', '.join(map(str, m))
1486 # and generate
1487 lab = cgi.escape(lab)
1488 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1489 l.append('</select>')
1490 return '\n'.join(l)
1491 # def checklist(self, ...)
1493 class MultilinkHTMLProperty(HTMLProperty):
1494 ''' Multilink HTMLProperty
1496 Also be iterable, returning a wrapper object like the Link case for
1497 each entry in the multilink.
1498 '''
1499 def __init__(self, *args, **kwargs):
1500 HTMLProperty.__init__(self, *args, **kwargs)
1501 if self._value:
1502 sortfun = make_sort_function(self._db, self._prop.classname)
1503 self._value.sort(sortfun)
1505 def __len__(self):
1506 ''' length of the multilink '''
1507 return len(self._value)
1509 def __getattr__(self, attr):
1510 ''' no extended attribute accesses make sense here '''
1511 raise AttributeError, attr
1513 def __getitem__(self, num):
1514 ''' iterate and return a new HTMLItem
1515 '''
1516 #print 'Multi.getitem', (self, num)
1517 value = self._value[num]
1518 if self._prop.classname == 'user':
1519 klass = HTMLUser
1520 else:
1521 klass = HTMLItem
1522 return klass(self._client, self._prop.classname, value)
1524 def __contains__(self, value):
1525 ''' Support the "in" operator. We have to make sure the passed-in
1526 value is a string first, not a HTMLProperty.
1527 '''
1528 return str(value) in self._value
1530 def reverse(self):
1531 ''' return the list in reverse order
1532 '''
1533 l = self._value[:]
1534 l.reverse()
1535 if self._prop.classname == 'user':
1536 klass = HTMLUser
1537 else:
1538 klass = HTMLItem
1539 return [klass(self._client, self._prop.classname, value) for value in l]
1541 def plain(self, escape=0):
1542 ''' Render a "plain" representation of the property
1543 '''
1544 self.view_check()
1546 linkcl = self._db.classes[self._prop.classname]
1547 k = linkcl.labelprop(1)
1548 labels = []
1549 for v in self._value:
1550 labels.append(linkcl.get(v, k))
1551 value = ', '.join(labels)
1552 if escape:
1553 value = cgi.escape(value)
1554 return value
1556 def field(self, size=30, showid=0):
1557 ''' Render a form edit field for the property
1559 If not editable, just display the value via plain().
1560 '''
1561 self.view_check()
1563 if not self.is_edit_ok():
1564 return self.plain()
1566 linkcl = self._db.getclass(self._prop.classname)
1567 value = self._value[:]
1568 # map the id to the label property
1569 if not linkcl.getkey():
1570 showid=1
1571 if not showid:
1572 k = linkcl.labelprop(1)
1573 value = lookupKeys(linkcl, k, value)
1574 value = cgi.escape(','.join(value))
1575 return self.input(name=self._formname,size=size,value=value)
1577 def menu(self, size=None, height=None, showid=0, additional=[],
1578 sort_on=None, **conditions):
1579 ''' Render a form select list for this property
1581 If not editable, just display the value via plain().
1582 '''
1583 self.view_check()
1585 if not self.is_edit_ok():
1586 return self.plain()
1588 value = self._value
1590 linkcl = self._db.getclass(self._prop.classname)
1591 if sort_on is None:
1592 sort_on = ('+', find_sort_key(linkcl))
1593 else:
1594 sort_on = ('+', sort_on)
1595 options = linkcl.filter(None, conditions, sort_on)
1596 height = height or min(len(options), 7)
1597 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1598 k = linkcl.labelprop(1)
1600 # make sure we list the current values if they're retired
1601 for val in value:
1602 if val not in options:
1603 options.insert(0, val)
1605 for optionid in options:
1606 # get the option value, and if it's None use an empty string
1607 option = linkcl.get(optionid, k) or ''
1609 # figure if this option is selected
1610 s = ''
1611 if optionid in value or option in value:
1612 s = 'selected="selected" '
1614 # figure the label
1615 if showid:
1616 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1617 else:
1618 lab = option
1619 # truncate if it's too long
1620 if size is not None and len(lab) > size:
1621 lab = lab[:size-3] + '...'
1622 if additional:
1623 m = []
1624 for propname in additional:
1625 m.append(linkcl.get(optionid, propname))
1626 lab = lab + ' (%s)'%', '.join(m)
1628 # and generate
1629 lab = cgi.escape(lab)
1630 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1631 lab))
1632 l.append('</select>')
1633 return '\n'.join(l)
1635 # set the propclasses for HTMLItem
1636 propclasses = (
1637 (hyperdb.String, StringHTMLProperty),
1638 (hyperdb.Number, NumberHTMLProperty),
1639 (hyperdb.Boolean, BooleanHTMLProperty),
1640 (hyperdb.Date, DateHTMLProperty),
1641 (hyperdb.Interval, IntervalHTMLProperty),
1642 (hyperdb.Password, PasswordHTMLProperty),
1643 (hyperdb.Link, LinkHTMLProperty),
1644 (hyperdb.Multilink, MultilinkHTMLProperty),
1645 )
1647 def make_sort_function(db, classname, sort_on=None):
1648 '''Make a sort function for a given class
1649 '''
1650 linkcl = db.getclass(classname)
1651 if sort_on is None:
1652 sort_on = find_sort_key(linkcl)
1653 def sortfunc(a, b):
1654 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1655 return sortfunc
1657 def find_sort_key(linkcl):
1658 if linkcl.getprops().has_key('order'):
1659 return 'order'
1660 else:
1661 return linkcl.labelprop()
1663 def handleListCGIValue(value):
1664 ''' Value is either a single item or a list of items. Each item has a
1665 .value that we're actually interested in.
1666 '''
1667 if isinstance(value, type([])):
1668 return [value.value for value in value]
1669 else:
1670 value = value.value.strip()
1671 if not value:
1672 return []
1673 return value.split(',')
1675 class ShowDict:
1676 ''' A convenience access to the :columns index parameters
1677 '''
1678 def __init__(self, columns):
1679 self.columns = {}
1680 for col in columns:
1681 self.columns[col] = 1
1682 def __getitem__(self, name):
1683 return self.columns.has_key(name)
1685 class HTMLRequest(HTMLInputMixin):
1686 '''The *request*, holding the CGI form and environment.
1688 - "form" the CGI form as a cgi.FieldStorage
1689 - "env" the CGI environment variables
1690 - "base" the base URL for this instance
1691 - "user" a HTMLUser instance for this user
1692 - "classname" the current classname (possibly None)
1693 - "template" the current template (suffix, also possibly None)
1695 Index args:
1697 - "columns" dictionary of the columns to display in an index page
1698 - "show" a convenience access to columns - request/show/colname will
1699 be true if the columns should be displayed, false otherwise
1700 - "sort" index sort column (direction, column name)
1701 - "group" index grouping property (direction, column name)
1702 - "filter" properties to filter the index on
1703 - "filterspec" values to filter the index on
1704 - "search_text" text to perform a full-text search on for an index
1705 '''
1706 def __init__(self, client):
1707 # _client is needed by HTMLInputMixin
1708 self._client = self.client = client
1710 # easier access vars
1711 self.form = client.form
1712 self.env = client.env
1713 self.base = client.base
1714 self.user = HTMLUser(client, 'user', client.userid)
1716 # store the current class name and action
1717 self.classname = client.classname
1718 self.template = client.template
1720 # the special char to use for special vars
1721 self.special_char = '@'
1723 HTMLInputMixin.__init__(self)
1725 self._post_init()
1727 def _post_init(self):
1728 ''' Set attributes based on self.form
1729 '''
1730 # extract the index display information from the form
1731 self.columns = []
1732 for name in ':columns @columns'.split():
1733 if self.form.has_key(name):
1734 self.special_char = name[0]
1735 self.columns = handleListCGIValue(self.form[name])
1736 break
1737 self.show = ShowDict(self.columns)
1739 # sorting
1740 self.sort = (None, None)
1741 for name in ':sort @sort'.split():
1742 if self.form.has_key(name):
1743 self.special_char = name[0]
1744 sort = self.form[name].value
1745 if sort.startswith('-'):
1746 self.sort = ('-', sort[1:])
1747 else:
1748 self.sort = ('+', sort)
1749 if self.form.has_key(self.special_char+'sortdir'):
1750 self.sort = ('-', self.sort[1])
1752 # grouping
1753 self.group = (None, None)
1754 for name in ':group @group'.split():
1755 if self.form.has_key(name):
1756 self.special_char = name[0]
1757 group = self.form[name].value
1758 if group.startswith('-'):
1759 self.group = ('-', group[1:])
1760 else:
1761 self.group = ('+', group)
1762 if self.form.has_key(self.special_char+'groupdir'):
1763 self.group = ('-', self.group[1])
1765 # filtering
1766 self.filter = []
1767 for name in ':filter @filter'.split():
1768 if self.form.has_key(name):
1769 self.special_char = name[0]
1770 self.filter = handleListCGIValue(self.form[name])
1772 self.filterspec = {}
1773 db = self.client.db
1774 if self.classname is not None:
1775 props = db.getclass(self.classname).getprops()
1776 for name in self.filter:
1777 if not self.form.has_key(name):
1778 continue
1779 prop = props[name]
1780 fv = self.form[name]
1781 if (isinstance(prop, hyperdb.Link) or
1782 isinstance(prop, hyperdb.Multilink)):
1783 self.filterspec[name] = lookupIds(db, prop,
1784 handleListCGIValue(fv))
1785 else:
1786 if isinstance(fv, type([])):
1787 self.filterspec[name] = [v.value for v in fv]
1788 else:
1789 self.filterspec[name] = fv.value
1791 # full-text search argument
1792 self.search_text = None
1793 for name in ':search_text @search_text'.split():
1794 if self.form.has_key(name):
1795 self.special_char = name[0]
1796 self.search_text = self.form[name].value
1798 # pagination - size and start index
1799 # figure batch args
1800 self.pagesize = 50
1801 for name in ':pagesize @pagesize'.split():
1802 if self.form.has_key(name):
1803 self.special_char = name[0]
1804 self.pagesize = int(self.form[name].value)
1806 self.startwith = 0
1807 for name in ':startwith @startwith'.split():
1808 if self.form.has_key(name):
1809 self.special_char = name[0]
1810 self.startwith = int(self.form[name].value)
1812 def updateFromURL(self, url):
1813 ''' Parse the URL for query args, and update my attributes using the
1814 values.
1815 '''
1816 env = {'QUERY_STRING': url}
1817 self.form = cgi.FieldStorage(environ=env)
1819 self._post_init()
1821 def update(self, kwargs):
1822 ''' Update my attributes using the keyword args
1823 '''
1824 self.__dict__.update(kwargs)
1825 if kwargs.has_key('columns'):
1826 self.show = ShowDict(self.columns)
1828 def description(self):
1829 ''' Return a description of the request - handle for the page title.
1830 '''
1831 s = [self.client.db.config.TRACKER_NAME]
1832 if self.classname:
1833 if self.client.nodeid:
1834 s.append('- %s%s'%(self.classname, self.client.nodeid))
1835 else:
1836 if self.template == 'item':
1837 s.append('- new %s'%self.classname)
1838 elif self.template == 'index':
1839 s.append('- %s index'%self.classname)
1840 else:
1841 s.append('- %s %s'%(self.classname, self.template))
1842 else:
1843 s.append('- home')
1844 return ' '.join(s)
1846 def __str__(self):
1847 d = {}
1848 d.update(self.__dict__)
1849 f = ''
1850 for k in self.form.keys():
1851 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1852 d['form'] = f
1853 e = ''
1854 for k,v in self.env.items():
1855 e += '\n %r=%r'%(k, v)
1856 d['env'] = e
1857 return '''
1858 form: %(form)s
1859 base: %(base)r
1860 classname: %(classname)r
1861 template: %(template)r
1862 columns: %(columns)r
1863 sort: %(sort)r
1864 group: %(group)r
1865 filter: %(filter)r
1866 search_text: %(search_text)r
1867 pagesize: %(pagesize)r
1868 startwith: %(startwith)r
1869 env: %(env)s
1870 '''%d
1872 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1873 filterspec=1):
1874 ''' return the current index args as form elements '''
1875 l = []
1876 sc = self.special_char
1877 s = self.input(type="hidden",name="%s",value="%s")
1878 if columns and self.columns:
1879 l.append(s%(sc+'columns', ','.join(self.columns)))
1880 if sort and self.sort[1] is not None:
1881 if self.sort[0] == '-':
1882 val = '-'+self.sort[1]
1883 else:
1884 val = self.sort[1]
1885 l.append(s%(sc+'sort', val))
1886 if group and self.group[1] is not None:
1887 if self.group[0] == '-':
1888 val = '-'+self.group[1]
1889 else:
1890 val = self.group[1]
1891 l.append(s%(sc+'group', val))
1892 if filter and self.filter:
1893 l.append(s%(sc+'filter', ','.join(self.filter)))
1894 if filterspec:
1895 for k,v in self.filterspec.items():
1896 if type(v) == type([]):
1897 l.append(s%(k, ','.join(v)))
1898 else:
1899 l.append(s%(k, v))
1900 if self.search_text:
1901 l.append(s%(sc+'search_text', self.search_text))
1902 l.append(s%(sc+'pagesize', self.pagesize))
1903 l.append(s%(sc+'startwith', self.startwith))
1904 return '\n'.join(l)
1906 def indexargs_url(self, url, args):
1907 ''' Embed the current index args in a URL
1908 '''
1909 sc = self.special_char
1910 l = ['%s=%s'%(k,v) for k,v in args.items()]
1912 # pull out the special values (prefixed by @ or :)
1913 specials = {}
1914 for key in args.keys():
1915 if key[0] in '@:':
1916 specials[key[1:]] = args[key]
1918 # ok, now handle the specials we received in the request
1919 if self.columns and not specials.has_key('columns'):
1920 l.append(sc+'columns=%s'%(','.join(self.columns)))
1921 if self.sort[1] is not None and not specials.has_key('sort'):
1922 if self.sort[0] == '-':
1923 val = '-'+self.sort[1]
1924 else:
1925 val = self.sort[1]
1926 l.append(sc+'sort=%s'%val)
1927 if self.group[1] is not None and not specials.has_key('group'):
1928 if self.group[0] == '-':
1929 val = '-'+self.group[1]
1930 else:
1931 val = self.group[1]
1932 l.append(sc+'group=%s'%val)
1933 if self.filter and not specials.has_key('filter'):
1934 l.append(sc+'filter=%s'%(','.join(self.filter)))
1935 if self.search_text and not specials.has_key('search_text'):
1936 l.append(sc+'search_text=%s'%self.search_text)
1937 if not specials.has_key('pagesize'):
1938 l.append(sc+'pagesize=%s'%self.pagesize)
1939 if not specials.has_key('startwith'):
1940 l.append(sc+'startwith=%s'%self.startwith)
1942 # finally, the remainder of the filter args in the request
1943 for k,v in self.filterspec.items():
1944 if not args.has_key(k):
1945 if type(v) == type([]):
1946 l.append('%s=%s'%(k, ','.join(v)))
1947 else:
1948 l.append('%s=%s'%(k, v))
1949 return '%s?%s'%(url, '&'.join(l))
1950 indexargs_href = indexargs_url
1952 def base_javascript(self):
1953 return '''
1954 <script type="text/javascript">
1955 submitted = false;
1956 function submit_once() {
1957 if (submitted) {
1958 alert("Your request is being processed.\\nPlease be patient.");
1959 event.returnValue = 0; // work-around for IE
1960 return 0;
1961 }
1962 submitted = true;
1963 return 1;
1964 }
1966 function help_window(helpurl, width, height) {
1967 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1968 }
1969 </script>
1970 '''%self.base
1972 def batch(self):
1973 ''' Return a batch object for results from the "current search"
1974 '''
1975 filterspec = self.filterspec
1976 sort = self.sort
1977 group = self.group
1979 # get the list of ids we're batching over
1980 klass = self.client.db.getclass(self.classname)
1981 if self.search_text:
1982 matches = self.client.db.indexer.search(
1983 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1984 else:
1985 matches = None
1986 l = klass.filter(matches, filterspec, sort, group)
1988 # return the batch object, using IDs only
1989 return Batch(self.client, l, self.pagesize, self.startwith,
1990 classname=self.classname)
1992 # extend the standard ZTUtils Batch object to remove dependency on
1993 # Acquisition and add a couple of useful methods
1994 class Batch(ZTUtils.Batch):
1995 ''' Use me to turn a list of items, or item ids of a given class, into a
1996 series of batches.
1998 ========= ========================================================
1999 Parameter Usage
2000 ========= ========================================================
2001 sequence a list of HTMLItems or item ids
2002 classname if sequence is a list of ids, this is the class of item
2003 size how big to make the sequence.
2004 start where to start (0-indexed) in the sequence.
2005 end where to end (0-indexed) in the sequence.
2006 orphan if the next batch would contain less items than this
2007 value, then it is combined with this batch
2008 overlap the number of items shared between adjacent batches
2009 ========= ========================================================
2011 Attributes: Note that the "start" attribute, unlike the
2012 argument, is a 1-based index (I know, lame). "first" is the
2013 0-based index. "length" is the actual number of elements in
2014 the batch.
2016 "sequence_length" is the length of the original, unbatched, sequence.
2017 '''
2018 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2019 overlap=0, classname=None):
2020 self.client = client
2021 self.last_index = self.last_item = None
2022 self.current_item = None
2023 self.classname = classname
2024 self.sequence_length = len(sequence)
2025 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2026 overlap)
2028 # overwrite so we can late-instantiate the HTMLItem instance
2029 def __getitem__(self, index):
2030 if index < 0:
2031 if index + self.end < self.first: raise IndexError, index
2032 return self._sequence[index + self.end]
2034 if index >= self.length:
2035 raise IndexError, index
2037 # move the last_item along - but only if the fetched index changes
2038 # (for some reason, index 0 is fetched twice)
2039 if index != self.last_index:
2040 self.last_item = self.current_item
2041 self.last_index = index
2043 item = self._sequence[index + self.first]
2044 if self.classname:
2045 # map the item ids to instances
2046 if self.classname == 'user':
2047 item = HTMLUser(self.client, self.classname, item)
2048 else:
2049 item = HTMLItem(self.client, self.classname, item)
2050 self.current_item = item
2051 return item
2053 def propchanged(self, property):
2054 ''' Detect if the property marked as being the group property
2055 changed in the last iteration fetch
2056 '''
2057 if (self.last_item is None or
2058 self.last_item[property] != self.current_item[property]):
2059 return 1
2060 return 0
2062 # override these 'cos we don't have access to acquisition
2063 def previous(self):
2064 if self.start == 1:
2065 return None
2066 return Batch(self.client, self._sequence, self._size,
2067 self.first - self._size + self.overlap, 0, self.orphan,
2068 self.overlap)
2070 def next(self):
2071 try:
2072 self._sequence[self.end]
2073 except IndexError:
2074 return None
2075 return Batch(self.client, self._sequence, self._size,
2076 self.end - self.overlap, 0, self.orphan, self.overlap)
2078 class TemplatingUtils:
2079 ''' Utilities for templating
2080 '''
2081 def __init__(self, client):
2082 self.client = client
2083 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2084 return Batch(self.client, sequence, size, start, end, orphan,
2085 overlap)