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