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?
949 if (is_anonymous and s.hasPermission('Web Registration', userid,
950 self._classname)):
951 return 1
953 # nope, no access here
954 return 0
956 class HTMLUserClass(HTMLUserPermission, HTMLClass):
957 pass
959 class HTMLUser(HTMLUserPermission, HTMLItem):
960 ''' Accesses through the *user* (a special case of item)
961 '''
962 def __init__(self, client, classname, nodeid, anonymous=0):
963 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
964 self._default_classname = client.classname
966 # used for security checks
967 self._security = client.db.security
969 _marker = []
970 def hasPermission(self, permission, classname=_marker):
971 ''' Determine if the user has the Permission.
973 The class being tested defaults to the template's class, but may
974 be overidden for this test by suppling an alternate classname.
975 '''
976 if classname is self._marker:
977 classname = self._default_classname
978 return self._security.hasPermission(permission, self._nodeid, classname)
980 class HTMLProperty(HTMLInputMixin, HTMLPermissions):
981 ''' String, Number, Date, Interval HTMLProperty
983 Has useful attributes:
985 _name the name of the property
986 _value the value of the property if any
988 A wrapper object which may be stringified for the plain() behaviour.
989 '''
990 def __init__(self, client, classname, nodeid, prop, name, value,
991 anonymous=0):
992 self._client = client
993 self._db = client.db
994 self._classname = classname
995 self._nodeid = nodeid
996 self._prop = prop
997 self._value = value
998 self._anonymous = anonymous
999 self._name = name
1000 if not anonymous:
1001 self._formname = '%s%s@%s'%(classname, nodeid, name)
1002 else:
1003 self._formname = name
1005 HTMLInputMixin.__init__(self)
1007 def __repr__(self):
1008 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
1009 self._prop, self._value)
1010 def __str__(self):
1011 return self.plain()
1012 def __cmp__(self, other):
1013 if isinstance(other, HTMLProperty):
1014 return cmp(self._value, other._value)
1015 return cmp(self._value, other)
1017 def isset(self):
1018 '''Is my _value None?'''
1019 return self._value is None
1021 def is_edit_ok(self):
1022 ''' Is the user allowed to Edit the current class?
1023 '''
1024 thing = HTMLDatabase(self._client)[self._classname]
1025 if self._nodeid:
1026 # this is a special-case for the User class where permission's
1027 # on a per-item basis :(
1028 thing = thing.getItem(self._nodeid)
1029 return thing.is_edit_ok()
1031 def is_view_ok(self):
1032 ''' Is the user allowed to View the current class?
1033 '''
1034 thing = HTMLDatabase(self._client)[self._classname]
1035 if self._nodeid:
1036 # this is a special-case for the User class where permission's
1037 # on a per-item basis :(
1038 thing = thing.getItem(self._nodeid)
1039 return thing.is_view_ok()
1041 class StringHTMLProperty(HTMLProperty):
1042 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
1043 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
1044 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
1045 def _hyper_repl(self, match):
1046 if match.group('url'):
1047 s = match.group('url')
1048 return '<a href="%s">%s</a>'%(s, s)
1049 elif match.group('email'):
1050 s = match.group('email')
1051 return '<a href="mailto:%s">%s</a>'%(s, s)
1052 else:
1053 s = match.group('item')
1054 s1 = match.group('class')
1055 s2 = match.group('id')
1056 try:
1057 # make sure s1 is a valid tracker classname
1058 cl = self._db.getclass(s1)
1059 if not cl.hasnode(s2):
1060 raise KeyError, 'oops'
1061 return '<a href="%s">%s%s</a>'%(s, s1, s2)
1062 except KeyError:
1063 return '%s%s'%(s1, s2)
1065 def hyperlinked(self):
1066 ''' Render a "hyperlinked" version of the text '''
1067 return self.plain(hyperlink=1)
1069 def plain(self, escape=0, hyperlink=0):
1070 '''Render a "plain" representation of the property
1072 - "escape" turns on/off HTML quoting
1073 - "hyperlink" turns on/off in-text hyperlinking of URLs, email
1074 addresses and designators
1075 '''
1076 self.view_check()
1078 if self._value is None:
1079 return ''
1080 if escape:
1081 s = cgi.escape(str(self._value))
1082 else:
1083 s = str(self._value)
1084 if hyperlink:
1085 # no, we *must* escape this text
1086 if not escape:
1087 s = cgi.escape(s)
1088 s = self.hyper_re.sub(self._hyper_repl, s)
1089 return s
1091 def stext(self, escape=0):
1092 ''' Render the value of the property as StructuredText.
1094 This requires the StructureText module to be installed separately.
1095 '''
1096 self.view_check()
1098 s = self.plain(escape=escape)
1099 if not StructuredText:
1100 return s
1101 return StructuredText(s,level=1,header=0)
1103 def field(self, size = 30):
1104 ''' Render the property as a field in HTML.
1106 If not editable, just display the value via plain().
1107 '''
1108 self.view_check()
1110 if self._value is None:
1111 value = ''
1112 else:
1113 value = cgi.escape(str(self._value))
1115 if self.is_edit_ok():
1116 value = '"'.join(value.split('"'))
1117 return self.input(name=self._formname,value=value,size=size)
1119 return self.plain()
1121 def multiline(self, escape=0, rows=5, cols=40):
1122 ''' Render a multiline form edit field for the property.
1124 If not editable, just display the plain() value in a <pre> tag.
1125 '''
1126 self.view_check()
1128 if self._value is None:
1129 value = ''
1130 else:
1131 value = cgi.escape(str(self._value))
1133 if self.is_edit_ok():
1134 value = '"'.join(value.split('"'))
1135 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
1136 self._formname, rows, cols, value)
1138 return '<pre>%s</pre>'%self.plain()
1140 def email(self, escape=1):
1141 ''' Render the value of the property as an obscured email address
1142 '''
1143 self.view_check()
1145 if self._value is None:
1146 value = ''
1147 else:
1148 value = str(self._value)
1149 if value.find('@') != -1:
1150 name, domain = value.split('@')
1151 domain = ' '.join(domain.split('.')[:-1])
1152 name = name.replace('.', ' ')
1153 value = '%s at %s ...'%(name, domain)
1154 else:
1155 value = value.replace('.', ' ')
1156 if escape:
1157 value = cgi.escape(value)
1158 return value
1160 class PasswordHTMLProperty(HTMLProperty):
1161 def plain(self):
1162 ''' Render a "plain" representation of the property
1163 '''
1164 self.view_check()
1166 if self._value is None:
1167 return ''
1168 return _('*encrypted*')
1170 def field(self, size = 30):
1171 ''' Render a form edit field for the property.
1173 If not editable, just display the value via plain().
1174 '''
1175 self.view_check()
1177 if self.is_edit_ok():
1178 return self.input(type="password", name=self._formname, size=size)
1180 return self.plain()
1182 def confirm(self, size = 30):
1183 ''' Render a second form edit field for the property, used for
1184 confirmation that the user typed the password correctly. Generates
1185 a field with name "@confirm@name".
1187 If not editable, display nothing.
1188 '''
1189 self.view_check()
1191 if self.is_edit_ok():
1192 return self.input(type="password",
1193 name="@confirm@%s"%self._formname, size=size)
1195 return ''
1197 class NumberHTMLProperty(HTMLProperty):
1198 def plain(self):
1199 ''' Render a "plain" representation of the property
1200 '''
1201 self.view_check()
1203 return str(self._value)
1205 def field(self, size = 30):
1206 ''' Render a form edit field for the property.
1208 If not editable, just display the value via plain().
1209 '''
1210 self.view_check()
1212 if self._value is None:
1213 value = ''
1214 else:
1215 value = cgi.escape(str(self._value))
1217 if self.is_edit_ok():
1218 value = '"'.join(value.split('"'))
1219 return self.input(name=self._formname,value=value,size=size)
1221 return self.plain()
1223 def __int__(self):
1224 ''' Return an int of me
1225 '''
1226 return int(self._value)
1228 def __float__(self):
1229 ''' Return a float of me
1230 '''
1231 return float(self._value)
1234 class BooleanHTMLProperty(HTMLProperty):
1235 def plain(self):
1236 ''' Render a "plain" representation of the property
1237 '''
1238 self.view_check()
1240 if self._value is None:
1241 return ''
1242 return self._value and "Yes" or "No"
1244 def field(self):
1245 ''' Render a form edit field for the property
1247 If not editable, just display the value via plain().
1248 '''
1249 self.view_check()
1251 if not self.is_edit_ok():
1252 return self.plain()
1254 checked = self._value and "checked" or ""
1255 if self._value:
1256 s = self.input(type="radio", name=self._formname, value="yes",
1257 checked="checked")
1258 s += 'Yes'
1259 s +=self.input(type="radio", name=self._formname, value="no")
1260 s += 'No'
1261 else:
1262 s = self.input(type="radio", name=self._formname, value="yes")
1263 s += 'Yes'
1264 s +=self.input(type="radio", name=self._formname, value="no",
1265 checked="checked")
1266 s += 'No'
1267 return s
1269 class DateHTMLProperty(HTMLProperty):
1270 def plain(self):
1271 ''' Render a "plain" representation of the property
1272 '''
1273 self.view_check()
1275 if self._value is None:
1276 return ''
1277 return str(self._value.local(self._db.getUserTimezone()))
1279 def now(self):
1280 ''' Return the current time.
1282 This is useful for defaulting a new value. Returns a
1283 DateHTMLProperty.
1284 '''
1285 self.view_check()
1287 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1288 self._prop, self._formname, date.Date('.'))
1290 def field(self, size = 30):
1291 ''' Render a form edit field for the property
1293 If not editable, just display the value via plain().
1294 '''
1295 self.view_check()
1297 if self._value is None:
1298 value = ''
1299 else:
1300 tz = self._db.getUserTimezone()
1301 value = cgi.escape(str(self._value.local(tz)))
1303 if self.is_edit_ok():
1304 value = '"'.join(value.split('"'))
1305 return self.input(name=self._formname,value=value,size=size)
1307 return self.plain()
1309 def reldate(self, pretty=1):
1310 ''' Render the interval between the date and now.
1312 If the "pretty" flag is true, then make the display pretty.
1313 '''
1314 self.view_check()
1316 if not self._value:
1317 return ''
1319 # figure the interval
1320 interval = self._value - date.Date('.')
1321 if pretty:
1322 return interval.pretty()
1323 return str(interval)
1325 _marker = []
1326 def pretty(self, format=_marker):
1327 ''' Render the date in a pretty format (eg. month names, spaces).
1329 The format string is a standard python strftime format string.
1330 Note that if the day is zero, and appears at the start of the
1331 string, then it'll be stripped from the output. This is handy
1332 for the situatin when a date only specifies a month and a year.
1333 '''
1334 self.view_check()
1336 if format is not self._marker:
1337 return self._value.pretty(format)
1338 else:
1339 return self._value.pretty()
1341 def local(self, offset):
1342 ''' Return the date/time as a local (timezone offset) date/time.
1343 '''
1344 self.view_check()
1346 return DateHTMLProperty(self._client, self._classname, self._nodeid,
1347 self._prop, self._formname, self._value.local(offset))
1349 class IntervalHTMLProperty(HTMLProperty):
1350 def plain(self):
1351 ''' Render a "plain" representation of the property
1352 '''
1353 self.view_check()
1355 if self._value is None:
1356 return ''
1357 return str(self._value)
1359 def pretty(self):
1360 ''' Render the interval in a pretty format (eg. "yesterday")
1361 '''
1362 self.view_check()
1364 return self._value.pretty()
1366 def field(self, size = 30):
1367 ''' Render a form edit field for the property
1369 If not editable, just display the value via plain().
1370 '''
1371 self.view_check()
1373 if self._value is None:
1374 value = ''
1375 else:
1376 value = cgi.escape(str(self._value))
1378 if is_edit_ok():
1379 value = '"'.join(value.split('"'))
1380 return self.input(name=self._formname,value=value,size=size)
1382 return self.plain()
1384 class LinkHTMLProperty(HTMLProperty):
1385 ''' Link HTMLProperty
1386 Include the above as well as being able to access the class
1387 information. Stringifying the object itself results in the value
1388 from the item being displayed. Accessing attributes of this object
1389 result in the appropriate entry from the class being queried for the
1390 property accessed (so item/assignedto/name would look up the user
1391 entry identified by the assignedto property on item, and then the
1392 name property of that user)
1393 '''
1394 def __init__(self, *args, **kw):
1395 HTMLProperty.__init__(self, *args, **kw)
1396 # if we're representing a form value, then the -1 from the form really
1397 # should be a None
1398 if str(self._value) == '-1':
1399 self._value = None
1401 def __getattr__(self, attr):
1402 ''' return a new HTMLItem '''
1403 #print 'Link.getattr', (self, attr, self._value)
1404 if not self._value:
1405 raise AttributeError, "Can't access missing value"
1406 if self._prop.classname == 'user':
1407 klass = HTMLUser
1408 else:
1409 klass = HTMLItem
1410 i = klass(self._client, self._prop.classname, self._value)
1411 return getattr(i, attr)
1413 def plain(self, escape=0):
1414 ''' Render a "plain" representation of the property
1415 '''
1416 self.view_check()
1418 if self._value is None:
1419 return ''
1420 linkcl = self._db.classes[self._prop.classname]
1421 k = linkcl.labelprop(1)
1422 value = str(linkcl.get(self._value, k))
1423 if escape:
1424 value = cgi.escape(value)
1425 return value
1427 def field(self, showid=0, size=None):
1428 ''' Render a form edit field for the property
1430 If not editable, just display the value via plain().
1431 '''
1432 self.view_check()
1434 if not self.is_edit_ok():
1435 return self.plain()
1437 # edit field
1438 linkcl = self._db.getclass(self._prop.classname)
1439 if self._value is None:
1440 value = ''
1441 else:
1442 k = linkcl.getkey()
1443 if k:
1444 value = linkcl.get(self._value, k)
1445 else:
1446 value = self._value
1447 value = cgi.escape(str(value))
1448 value = '"'.join(value.split('"'))
1449 return '<input name="%s" value="%s" size="%s">'%(self._formname,
1450 value, size)
1452 def menu(self, size=None, height=None, showid=0, additional=[],
1453 sort_on=None, **conditions):
1454 ''' Render a form select list for this property
1456 If not editable, just display the value via plain().
1457 '''
1458 self.view_check()
1460 if not self.is_edit_ok():
1461 return self.plain()
1463 value = self._value
1465 linkcl = self._db.getclass(self._prop.classname)
1466 l = ['<select name="%s">'%self._formname]
1467 k = linkcl.labelprop(1)
1468 s = ''
1469 if value is None:
1470 s = 'selected="selected" '
1471 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1472 if linkcl.getprops().has_key('order'):
1473 sort_on = ('+', 'order')
1474 else:
1475 if sort_on is None:
1476 sort_on = ('+', linkcl.labelprop())
1477 else:
1478 sort_on = ('+', sort_on)
1479 options = linkcl.filter(None, conditions, sort_on, (None, None))
1481 # make sure we list the current value if it's retired
1482 if self._value and self._value not in options:
1483 options.insert(0, self._value)
1485 for optionid in options:
1486 # get the option value, and if it's None use an empty string
1487 option = linkcl.get(optionid, k) or ''
1489 # figure if this option is selected
1490 s = ''
1491 if value in [optionid, option]:
1492 s = 'selected="selected" '
1494 # figure the label
1495 if showid:
1496 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1497 else:
1498 lab = option
1500 # truncate if it's too long
1501 if size is not None and len(lab) > size:
1502 lab = lab[:size-3] + '...'
1503 if additional:
1504 m = []
1505 for propname in additional:
1506 m.append(linkcl.get(optionid, propname))
1507 lab = lab + ' (%s)'%', '.join(map(str, m))
1509 # and generate
1510 lab = cgi.escape(lab)
1511 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1512 l.append('</select>')
1513 return '\n'.join(l)
1514 # def checklist(self, ...)
1516 class MultilinkHTMLProperty(HTMLProperty):
1517 ''' Multilink HTMLProperty
1519 Also be iterable, returning a wrapper object like the Link case for
1520 each entry in the multilink.
1521 '''
1522 def __init__(self, *args, **kwargs):
1523 HTMLProperty.__init__(self, *args, **kwargs)
1524 if self._value:
1525 sortfun = make_sort_function(self._db, self._prop.classname)
1526 self._value.sort(sortfun)
1528 def __len__(self):
1529 ''' length of the multilink '''
1530 return len(self._value)
1532 def __getattr__(self, attr):
1533 ''' no extended attribute accesses make sense here '''
1534 raise AttributeError, attr
1536 def __getitem__(self, num):
1537 ''' iterate and return a new HTMLItem
1538 '''
1539 #print 'Multi.getitem', (self, num)
1540 value = self._value[num]
1541 if self._prop.classname == 'user':
1542 klass = HTMLUser
1543 else:
1544 klass = HTMLItem
1545 return klass(self._client, self._prop.classname, value)
1547 def __contains__(self, value):
1548 ''' Support the "in" operator. We have to make sure the passed-in
1549 value is a string first, not a HTMLProperty.
1550 '''
1551 return str(value) in self._value
1553 def isset(self):
1554 '''Is my _value []?'''
1555 return self._value == []
1557 def reverse(self):
1558 ''' return the list in reverse order
1559 '''
1560 l = self._value[:]
1561 l.reverse()
1562 if self._prop.classname == 'user':
1563 klass = HTMLUser
1564 else:
1565 klass = HTMLItem
1566 return [klass(self._client, self._prop.classname, value) for value in l]
1568 def plain(self, escape=0):
1569 ''' Render a "plain" representation of the property
1570 '''
1571 self.view_check()
1573 linkcl = self._db.classes[self._prop.classname]
1574 k = linkcl.labelprop(1)
1575 labels = []
1576 for v in self._value:
1577 labels.append(linkcl.get(v, k))
1578 value = ', '.join(labels)
1579 if escape:
1580 value = cgi.escape(value)
1581 return value
1583 def field(self, size=30, showid=0):
1584 ''' Render a form edit field for the property
1586 If not editable, just display the value via plain().
1587 '''
1588 self.view_check()
1590 if not self.is_edit_ok():
1591 return self.plain()
1593 linkcl = self._db.getclass(self._prop.classname)
1594 value = self._value[:]
1595 # map the id to the label property
1596 if not linkcl.getkey():
1597 showid=1
1598 if not showid:
1599 k = linkcl.labelprop(1)
1600 value = lookupKeys(linkcl, k, value)
1601 value = cgi.escape(','.join(value))
1602 return self.input(name=self._formname,size=size,value=value)
1604 def menu(self, size=None, height=None, showid=0, additional=[],
1605 sort_on=None, **conditions):
1606 ''' Render a form select list for this property
1608 If not editable, just display the value via plain().
1609 '''
1610 self.view_check()
1612 if not self.is_edit_ok():
1613 return self.plain()
1615 value = self._value
1617 linkcl = self._db.getclass(self._prop.classname)
1618 if sort_on is None:
1619 sort_on = ('+', find_sort_key(linkcl))
1620 else:
1621 sort_on = ('+', sort_on)
1622 options = linkcl.filter(None, conditions, sort_on)
1623 height = height or min(len(options), 7)
1624 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1625 k = linkcl.labelprop(1)
1627 # make sure we list the current values if they're retired
1628 for val in value:
1629 if val not in options:
1630 options.insert(0, val)
1632 for optionid in options:
1633 # get the option value, and if it's None use an empty string
1634 option = linkcl.get(optionid, k) or ''
1636 # figure if this option is selected
1637 s = ''
1638 if optionid in value or option in value:
1639 s = 'selected="selected" '
1641 # figure the label
1642 if showid:
1643 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1644 else:
1645 lab = option
1646 # truncate if it's too long
1647 if size is not None and len(lab) > size:
1648 lab = lab[:size-3] + '...'
1649 if additional:
1650 m = []
1651 for propname in additional:
1652 m.append(linkcl.get(optionid, propname))
1653 lab = lab + ' (%s)'%', '.join(m)
1655 # and generate
1656 lab = cgi.escape(lab)
1657 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1658 lab))
1659 l.append('</select>')
1660 return '\n'.join(l)
1662 # set the propclasses for HTMLItem
1663 propclasses = (
1664 (hyperdb.String, StringHTMLProperty),
1665 (hyperdb.Number, NumberHTMLProperty),
1666 (hyperdb.Boolean, BooleanHTMLProperty),
1667 (hyperdb.Date, DateHTMLProperty),
1668 (hyperdb.Interval, IntervalHTMLProperty),
1669 (hyperdb.Password, PasswordHTMLProperty),
1670 (hyperdb.Link, LinkHTMLProperty),
1671 (hyperdb.Multilink, MultilinkHTMLProperty),
1672 )
1674 def make_sort_function(db, classname, sort_on=None):
1675 '''Make a sort function for a given class
1676 '''
1677 linkcl = db.getclass(classname)
1678 if sort_on is None:
1679 sort_on = find_sort_key(linkcl)
1680 def sortfunc(a, b):
1681 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1682 return sortfunc
1684 def find_sort_key(linkcl):
1685 if linkcl.getprops().has_key('order'):
1686 return 'order'
1687 else:
1688 return linkcl.labelprop()
1690 def handleListCGIValue(value):
1691 ''' Value is either a single item or a list of items. Each item has a
1692 .value that we're actually interested in.
1693 '''
1694 if isinstance(value, type([])):
1695 return [value.value for value in value]
1696 else:
1697 value = value.value.strip()
1698 if not value:
1699 return []
1700 return value.split(',')
1702 class ShowDict:
1703 ''' A convenience access to the :columns index parameters
1704 '''
1705 def __init__(self, columns):
1706 self.columns = {}
1707 for col in columns:
1708 self.columns[col] = 1
1709 def __getitem__(self, name):
1710 return self.columns.has_key(name)
1712 class HTMLRequest(HTMLInputMixin):
1713 '''The *request*, holding the CGI form and environment.
1715 - "form" the CGI form as a cgi.FieldStorage
1716 - "env" the CGI environment variables
1717 - "base" the base URL for this instance
1718 - "user" a HTMLUser instance for this user
1719 - "classname" the current classname (possibly None)
1720 - "template" the current template (suffix, also possibly None)
1722 Index args:
1724 - "columns" dictionary of the columns to display in an index page
1725 - "show" a convenience access to columns - request/show/colname will
1726 be true if the columns should be displayed, false otherwise
1727 - "sort" index sort column (direction, column name)
1728 - "group" index grouping property (direction, column name)
1729 - "filter" properties to filter the index on
1730 - "filterspec" values to filter the index on
1731 - "search_text" text to perform a full-text search on for an index
1732 '''
1733 def __init__(self, client):
1734 # _client is needed by HTMLInputMixin
1735 self._client = self.client = client
1737 # easier access vars
1738 self.form = client.form
1739 self.env = client.env
1740 self.base = client.base
1741 self.user = HTMLUser(client, 'user', client.userid)
1743 # store the current class name and action
1744 self.classname = client.classname
1745 self.template = client.template
1747 # the special char to use for special vars
1748 self.special_char = '@'
1750 HTMLInputMixin.__init__(self)
1752 self._post_init()
1754 def _post_init(self):
1755 ''' Set attributes based on self.form
1756 '''
1757 # extract the index display information from the form
1758 self.columns = []
1759 for name in ':columns @columns'.split():
1760 if self.form.has_key(name):
1761 self.special_char = name[0]
1762 self.columns = handleListCGIValue(self.form[name])
1763 break
1764 self.show = ShowDict(self.columns)
1766 # sorting
1767 self.sort = (None, None)
1768 for name in ':sort @sort'.split():
1769 if self.form.has_key(name):
1770 self.special_char = name[0]
1771 sort = self.form[name].value
1772 if sort.startswith('-'):
1773 self.sort = ('-', sort[1:])
1774 else:
1775 self.sort = ('+', sort)
1776 if self.form.has_key(self.special_char+'sortdir'):
1777 self.sort = ('-', self.sort[1])
1779 # grouping
1780 self.group = (None, None)
1781 for name in ':group @group'.split():
1782 if self.form.has_key(name):
1783 self.special_char = name[0]
1784 group = self.form[name].value
1785 if group.startswith('-'):
1786 self.group = ('-', group[1:])
1787 else:
1788 self.group = ('+', group)
1789 if self.form.has_key(self.special_char+'groupdir'):
1790 self.group = ('-', self.group[1])
1792 # filtering
1793 self.filter = []
1794 for name in ':filter @filter'.split():
1795 if self.form.has_key(name):
1796 self.special_char = name[0]
1797 self.filter = handleListCGIValue(self.form[name])
1799 self.filterspec = {}
1800 db = self.client.db
1801 if self.classname is not None:
1802 props = db.getclass(self.classname).getprops()
1803 for name in self.filter:
1804 if not self.form.has_key(name):
1805 continue
1806 prop = props[name]
1807 fv = self.form[name]
1808 if (isinstance(prop, hyperdb.Link) or
1809 isinstance(prop, hyperdb.Multilink)):
1810 self.filterspec[name] = lookupIds(db, prop,
1811 handleListCGIValue(fv))
1812 else:
1813 if isinstance(fv, type([])):
1814 self.filterspec[name] = [v.value for v in fv]
1815 else:
1816 self.filterspec[name] = fv.value
1818 # full-text search argument
1819 self.search_text = None
1820 for name in ':search_text @search_text'.split():
1821 if self.form.has_key(name):
1822 self.special_char = name[0]
1823 self.search_text = self.form[name].value
1825 # pagination - size and start index
1826 # figure batch args
1827 self.pagesize = 50
1828 for name in ':pagesize @pagesize'.split():
1829 if self.form.has_key(name):
1830 self.special_char = name[0]
1831 self.pagesize = int(self.form[name].value)
1833 self.startwith = 0
1834 for name in ':startwith @startwith'.split():
1835 if self.form.has_key(name):
1836 self.special_char = name[0]
1837 self.startwith = int(self.form[name].value)
1839 def updateFromURL(self, url):
1840 ''' Parse the URL for query args, and update my attributes using the
1841 values.
1842 '''
1843 env = {'QUERY_STRING': url}
1844 self.form = cgi.FieldStorage(environ=env)
1846 self._post_init()
1848 def update(self, kwargs):
1849 ''' Update my attributes using the keyword args
1850 '''
1851 self.__dict__.update(kwargs)
1852 if kwargs.has_key('columns'):
1853 self.show = ShowDict(self.columns)
1855 def description(self):
1856 ''' Return a description of the request - handle for the page title.
1857 '''
1858 s = [self.client.db.config.TRACKER_NAME]
1859 if self.classname:
1860 if self.client.nodeid:
1861 s.append('- %s%s'%(self.classname, self.client.nodeid))
1862 else:
1863 if self.template == 'item':
1864 s.append('- new %s'%self.classname)
1865 elif self.template == 'index':
1866 s.append('- %s index'%self.classname)
1867 else:
1868 s.append('- %s %s'%(self.classname, self.template))
1869 else:
1870 s.append('- home')
1871 return ' '.join(s)
1873 def __str__(self):
1874 d = {}
1875 d.update(self.__dict__)
1876 f = ''
1877 for k in self.form.keys():
1878 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1879 d['form'] = f
1880 e = ''
1881 for k,v in self.env.items():
1882 e += '\n %r=%r'%(k, v)
1883 d['env'] = e
1884 return '''
1885 form: %(form)s
1886 base: %(base)r
1887 classname: %(classname)r
1888 template: %(template)r
1889 columns: %(columns)r
1890 sort: %(sort)r
1891 group: %(group)r
1892 filter: %(filter)r
1893 search_text: %(search_text)r
1894 pagesize: %(pagesize)r
1895 startwith: %(startwith)r
1896 env: %(env)s
1897 '''%d
1899 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1900 filterspec=1):
1901 ''' return the current index args as form elements '''
1902 l = []
1903 sc = self.special_char
1904 s = self.input(type="hidden",name="%s",value="%s")
1905 if columns and self.columns:
1906 l.append(s%(sc+'columns', ','.join(self.columns)))
1907 if sort and self.sort[1] is not None:
1908 if self.sort[0] == '-':
1909 val = '-'+self.sort[1]
1910 else:
1911 val = self.sort[1]
1912 l.append(s%(sc+'sort', val))
1913 if group and self.group[1] is not None:
1914 if self.group[0] == '-':
1915 val = '-'+self.group[1]
1916 else:
1917 val = self.group[1]
1918 l.append(s%(sc+'group', val))
1919 if filter and self.filter:
1920 l.append(s%(sc+'filter', ','.join(self.filter)))
1921 if filterspec:
1922 for k,v in self.filterspec.items():
1923 if type(v) == type([]):
1924 l.append(s%(k, ','.join(v)))
1925 else:
1926 l.append(s%(k, v))
1927 if self.search_text:
1928 l.append(s%(sc+'search_text', self.search_text))
1929 l.append(s%(sc+'pagesize', self.pagesize))
1930 l.append(s%(sc+'startwith', self.startwith))
1931 return '\n'.join(l)
1933 def indexargs_url(self, url, args):
1934 ''' Embed the current index args in a URL
1935 '''
1936 sc = self.special_char
1937 l = ['%s=%s'%(k,v) for k,v in args.items()]
1939 # pull out the special values (prefixed by @ or :)
1940 specials = {}
1941 for key in args.keys():
1942 if key[0] in '@:':
1943 specials[key[1:]] = args[key]
1945 # ok, now handle the specials we received in the request
1946 if self.columns and not specials.has_key('columns'):
1947 l.append(sc+'columns=%s'%(','.join(self.columns)))
1948 if self.sort[1] is not None and not specials.has_key('sort'):
1949 if self.sort[0] == '-':
1950 val = '-'+self.sort[1]
1951 else:
1952 val = self.sort[1]
1953 l.append(sc+'sort=%s'%val)
1954 if self.group[1] is not None and not specials.has_key('group'):
1955 if self.group[0] == '-':
1956 val = '-'+self.group[1]
1957 else:
1958 val = self.group[1]
1959 l.append(sc+'group=%s'%val)
1960 if self.filter and not specials.has_key('filter'):
1961 l.append(sc+'filter=%s'%(','.join(self.filter)))
1962 if self.search_text and not specials.has_key('search_text'):
1963 l.append(sc+'search_text=%s'%self.search_text)
1964 if not specials.has_key('pagesize'):
1965 l.append(sc+'pagesize=%s'%self.pagesize)
1966 if not specials.has_key('startwith'):
1967 l.append(sc+'startwith=%s'%self.startwith)
1969 # finally, the remainder of the filter args in the request
1970 for k,v in self.filterspec.items():
1971 if not args.has_key(k):
1972 if type(v) == type([]):
1973 l.append('%s=%s'%(k, ','.join(v)))
1974 else:
1975 l.append('%s=%s'%(k, v))
1976 return '%s?%s'%(url, '&'.join(l))
1977 indexargs_href = indexargs_url
1979 def base_javascript(self):
1980 return '''
1981 <script type="text/javascript">
1982 submitted = false;
1983 function submit_once() {
1984 if (submitted) {
1985 alert("Your request is being processed.\\nPlease be patient.");
1986 event.returnValue = 0; // work-around for IE
1987 return 0;
1988 }
1989 submitted = true;
1990 return 1;
1991 }
1993 function help_window(helpurl, width, height) {
1994 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1995 }
1996 </script>
1997 '''%self.base
1999 def batch(self):
2000 ''' Return a batch object for results from the "current search"
2001 '''
2002 filterspec = self.filterspec
2003 sort = self.sort
2004 group = self.group
2006 # get the list of ids we're batching over
2007 klass = self.client.db.getclass(self.classname)
2008 if self.search_text:
2009 matches = self.client.db.indexer.search(
2010 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
2011 else:
2012 matches = None
2013 l = klass.filter(matches, filterspec, sort, group)
2015 # return the batch object, using IDs only
2016 return Batch(self.client, l, self.pagesize, self.startwith,
2017 classname=self.classname)
2019 # extend the standard ZTUtils Batch object to remove dependency on
2020 # Acquisition and add a couple of useful methods
2021 class Batch(ZTUtils.Batch):
2022 ''' Use me to turn a list of items, or item ids of a given class, into a
2023 series of batches.
2025 ========= ========================================================
2026 Parameter Usage
2027 ========= ========================================================
2028 sequence a list of HTMLItems or item ids
2029 classname if sequence is a list of ids, this is the class of item
2030 size how big to make the sequence.
2031 start where to start (0-indexed) in the sequence.
2032 end where to end (0-indexed) in the sequence.
2033 orphan if the next batch would contain less items than this
2034 value, then it is combined with this batch
2035 overlap the number of items shared between adjacent batches
2036 ========= ========================================================
2038 Attributes: Note that the "start" attribute, unlike the
2039 argument, is a 1-based index (I know, lame). "first" is the
2040 0-based index. "length" is the actual number of elements in
2041 the batch.
2043 "sequence_length" is the length of the original, unbatched, sequence.
2044 '''
2045 def __init__(self, client, sequence, size, start, end=0, orphan=0,
2046 overlap=0, classname=None):
2047 self.client = client
2048 self.last_index = self.last_item = None
2049 self.current_item = None
2050 self.classname = classname
2051 self.sequence_length = len(sequence)
2052 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
2053 overlap)
2055 # overwrite so we can late-instantiate the HTMLItem instance
2056 def __getitem__(self, index):
2057 if index < 0:
2058 if index + self.end < self.first: raise IndexError, index
2059 return self._sequence[index + self.end]
2061 if index >= self.length:
2062 raise IndexError, index
2064 # move the last_item along - but only if the fetched index changes
2065 # (for some reason, index 0 is fetched twice)
2066 if index != self.last_index:
2067 self.last_item = self.current_item
2068 self.last_index = index
2070 item = self._sequence[index + self.first]
2071 if self.classname:
2072 # map the item ids to instances
2073 if self.classname == 'user':
2074 item = HTMLUser(self.client, self.classname, item)
2075 else:
2076 item = HTMLItem(self.client, self.classname, item)
2077 self.current_item = item
2078 return item
2080 def propchanged(self, property):
2081 ''' Detect if the property marked as being the group property
2082 changed in the last iteration fetch
2083 '''
2084 if (self.last_item is None or
2085 self.last_item[property] != self.current_item[property]):
2086 return 1
2087 return 0
2089 # override these 'cos we don't have access to acquisition
2090 def previous(self):
2091 if self.start == 1:
2092 return None
2093 return Batch(self.client, self._sequence, self._size,
2094 self.first - self._size + self.overlap, 0, self.orphan,
2095 self.overlap)
2097 def next(self):
2098 try:
2099 self._sequence[self.end]
2100 except IndexError:
2101 return None
2102 return Batch(self.client, self._sequence, self._size,
2103 self.end - self.overlap, 0, self.orphan, self.overlap)
2105 class TemplatingUtils:
2106 ''' Utilities for templating
2107 '''
2108 def __init__(self, client):
2109 self.client = client
2110 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
2111 return Batch(self.client, sequence, size, start, end, orphan,
2112 overlap)
2114 def url_quote(self, url):
2115 '''URL-quote the supplied text.'''
2116 return urllib.quote(url)
2118 def html_quote(self, html):
2119 '''HTML-quote the supplied text.'''
2120 return cgi.escape(url)