1 from __future__ import nested_scopes
3 import sys, cgi, urllib, os, re, os.path, time, errno, mimetypes
5 from roundup import hyperdb, date, rcsv
6 from roundup.i18n import _
8 try:
9 import cPickle as pickle
10 except ImportError:
11 import pickle
12 try:
13 import cStringIO as StringIO
14 except ImportError:
15 import StringIO
16 try:
17 import StructuredText
18 except ImportError:
19 StructuredText = None
21 # bring in the templating support
22 from roundup.cgi.PageTemplates import PageTemplate
23 from roundup.cgi.PageTemplates.Expressions import getEngine
24 from roundup.cgi.TAL.TALInterpreter import TALInterpreter
25 from roundup.cgi import ZTUtils
27 def input_html4(**attrs):
28 """Generate an 'input' (html4) element with given attributes"""
29 return '<input %s>'%' '.join(['%s="%s"'%item for item in attrs.items()])
31 def input_xhtml(**attrs):
32 """Generate an 'input' (xhtml) element with given attributes"""
33 return '<input %s/>'%' '.join(['%s="%s"'%item for item in attrs.items()])
35 class NoTemplate(Exception):
36 pass
38 def find_template(dir, name, extension):
39 ''' Find a template in the nominated dir
40 '''
41 # find the source
42 if extension:
43 filename = '%s.%s'%(name, extension)
44 else:
45 filename = name
47 # try old-style
48 src = os.path.join(dir, filename)
49 if os.path.exists(src):
50 return (src, filename)
52 # try with a .html extension (new-style)
53 filename = filename + '.html'
54 src = os.path.join(dir, filename)
55 if os.path.exists(src):
56 return (src, filename)
58 # no extension == no generic template is possible
59 if not extension:
60 raise NoTemplate, 'Template file "%s" doesn\'t exist'%name
62 # try for a _generic template
63 generic = '_generic.%s'%extension
64 src = os.path.join(dir, generic)
65 if os.path.exists(src):
66 return (src, generic)
68 # finally, try _generic.html
69 generic = generic + '.html'
70 src = os.path.join(dir, generic)
71 if os.path.exists(src):
72 return (src, generic)
74 raise NoTemplate, 'No template file exists for templating "%s" '\
75 'with template "%s" (neither "%s" nor "%s")'%(name, extension,
76 filename, generic)
78 class Templates:
79 templates = {}
81 def __init__(self, dir):
82 self.dir = dir
84 def precompileTemplates(self):
85 ''' Go through a directory and precompile all the templates therein
86 '''
87 for filename in os.listdir(self.dir):
88 if os.path.isdir(filename): continue
89 if '.' in filename:
90 name, extension = filename.split('.')
91 self.get(name, extension)
92 else:
93 self.get(filename, None)
95 def get(self, name, extension=None):
96 ''' Interface to get a template, possibly loading a compiled template.
98 "name" and "extension" indicate the template we're after, which in
99 most cases will be "name.extension". If "extension" is None, then
100 we look for a template just called "name" with no extension.
102 If the file "name.extension" doesn't exist, we look for
103 "_generic.extension" as a fallback.
104 '''
105 # default the name to "home"
106 if name is None:
107 name = 'home'
108 elif extension is None and '.' in name:
109 # split name
110 name, extension = name.split('.')
112 # find the source
113 src, filename = find_template(self.dir, name, extension)
115 # has it changed?
116 try:
117 stime = os.stat(src)[os.path.stat.ST_MTIME]
118 except os.error, error:
119 if error.errno != errno.ENOENT:
120 raise
122 if self.templates.has_key(src) and \
123 stime < self.templates[src].mtime:
124 # compiled template is up to date
125 return self.templates[src]
127 # compile the template
128 self.templates[src] = pt = RoundupPageTemplate()
129 # use pt_edit so we can pass the content_type guess too
130 content_type = mimetypes.guess_type(filename)[0] or 'text/html'
131 pt.pt_edit(open(src).read(), content_type)
132 pt.id = filename
133 pt.mtime = time.time()
134 return pt
136 def __getitem__(self, name):
137 name, extension = os.path.splitext(name)
138 if extension:
139 extension = extension[1:]
140 try:
141 return self.get(name, extension)
142 except NoTemplate, message:
143 raise KeyError, message
145 class RoundupPageTemplate(PageTemplate.PageTemplate):
146 ''' A Roundup-specific PageTemplate.
148 Interrogate the client to set up the various template variables to
149 be available:
151 *context*
152 this is one of three things:
153 1. None - we're viewing a "home" page
154 2. The current class of item being displayed. This is an HTMLClass
155 instance.
156 3. The current item from the database, if we're viewing a specific
157 item, as an HTMLItem instance.
158 *request*
159 Includes information about the current request, including:
160 - the url
161 - the current index information (``filterspec``, ``filter`` args,
162 ``properties``, etc) parsed out of the form.
163 - methods for easy filterspec link generation
164 - *user*, the current user node as an HTMLItem instance
165 - *form*, the current CGI form information as a FieldStorage
166 *config*
167 The current tracker config.
168 *db*
169 The current database, used to access arbitrary database items.
170 *utils*
171 This is a special class that has its base in the TemplatingUtils
172 class in this file. If the tracker interfaces module defines a
173 TemplatingUtils class then it is mixed in, overriding the methods
174 in the base class.
175 '''
176 def getContext(self, client, classname, request):
177 # construct the TemplatingUtils class
178 utils = TemplatingUtils
179 if hasattr(client.instance.interfaces, 'TemplatingUtils'):
180 class utils(client.instance.interfaces.TemplatingUtils, utils):
181 pass
183 c = {
184 'options': {},
185 'nothing': None,
186 'request': request,
187 'db': HTMLDatabase(client),
188 'config': client.instance.config,
189 'tracker': client.instance,
190 'utils': utils(client),
191 'templates': Templates(client.instance.config.TEMPLATES),
192 }
193 # add in the item if there is one
194 if client.nodeid:
195 if classname == 'user':
196 c['context'] = HTMLUser(client, classname, client.nodeid,
197 anonymous=1)
198 else:
199 c['context'] = HTMLItem(client, classname, client.nodeid,
200 anonymous=1)
201 elif client.db.classes.has_key(classname):
202 c['context'] = HTMLClass(client, classname, anonymous=1)
203 return c
205 def render(self, client, classname, request, **options):
206 """Render this Page Template"""
208 if not self._v_cooked:
209 self._cook()
211 __traceback_supplement__ = (PageTemplate.PageTemplateTracebackSupplement, self)
213 if self._v_errors:
214 raise PageTemplate.PTRuntimeError, \
215 'Page Template %s has errors.'%self.id
217 # figure the context
218 classname = classname or client.classname
219 request = request or HTMLRequest(client)
220 c = self.getContext(client, classname, request)
221 c.update({'options': options})
223 # and go
224 output = StringIO.StringIO()
225 TALInterpreter(self._v_program, self.macros,
226 getEngine().getContext(c), output, tal=1, strictinsert=0)()
227 return output.getvalue()
229 class HTMLDatabase:
230 ''' Return HTMLClasses for valid class fetches
231 '''
232 def __init__(self, client):
233 self._client = client
234 self._db = client.db
236 # we want config to be exposed
237 self.config = client.db.config
239 def __getitem__(self, item, desre=re.compile(r'(?P<cl>\w+)(?P<id>[-\d]+)')):
240 # check to see if we're actually accessing an item
241 m = desre.match(item)
242 if m:
243 self._client.db.getclass(m.group('cl'))
244 return HTMLItem(self._client, m.group('cl'), m.group('id'))
245 else:
246 self._client.db.getclass(item)
247 return HTMLClass(self._client, item)
249 def __getattr__(self, attr):
250 try:
251 return self[attr]
252 except KeyError:
253 raise AttributeError, attr
255 def classes(self):
256 l = self._client.db.classes.keys()
257 l.sort()
258 return [HTMLClass(self._client, cn) for cn in l]
260 def lookupIds(db, prop, ids, num_re=re.compile('-?\d+')):
261 cl = db.getclass(prop.classname)
262 l = []
263 for entry in ids:
264 if num_re.match(entry):
265 l.append(entry)
266 else:
267 try:
268 l.append(cl.lookup(entry))
269 except KeyError:
270 # ignore invalid keys
271 pass
272 return l
274 class HTMLPermissions:
275 ''' Helpers that provide answers to commonly asked Permission questions.
276 '''
277 def is_edit_ok(self):
278 ''' Is the user allowed to Edit the current class?
279 '''
280 return self._db.security.hasPermission('Edit', self._client.userid,
281 self._classname)
282 def is_view_ok(self):
283 ''' Is the user allowed to View the current class?
284 '''
285 return self._db.security.hasPermission('View', self._client.userid,
286 self._classname)
287 def is_only_view_ok(self):
288 ''' Is the user only allowed to View (ie. not Edit) the current class?
289 '''
290 return self.is_view_ok() and not self.is_edit_ok()
292 class HTMLClass(HTMLPermissions):
293 ''' Accesses through a class (either through *class* or *db.<classname>*)
294 '''
295 def __init__(self, client, classname, anonymous=0):
296 self._client = client
297 self._db = client.db
298 self._anonymous = anonymous
300 # we want classname to be exposed, but _classname gives a
301 # consistent API for extending Class/Item
302 self._classname = self.classname = classname
303 self._klass = self._db.getclass(self.classname)
304 self._props = self._klass.getprops()
306 html_version = 'html4'
307 if hasattr(self._client.instance.config, 'HTML_VERSION'):
308 html_version = self._client.instance.config.HTML_VERSION
309 if html_version == 'xhtml':
310 self.input = input_xhtml
311 else:
312 self.input = input_html4
314 def __repr__(self):
315 return '<HTMLClass(0x%x) %s>'%(id(self), self.classname)
317 def __getitem__(self, item):
318 ''' return an HTMLProperty instance
319 '''
320 #print 'HTMLClass.getitem', (self, item)
322 # we don't exist
323 if item == 'id':
324 return None
326 # get the property
327 prop = self._props[item]
329 # look up the correct HTMLProperty class
330 form = self._client.form
331 for klass, htmlklass in propclasses:
332 if not isinstance(prop, klass):
333 continue
334 if form.has_key(item):
335 if isinstance(prop, hyperdb.Multilink):
336 value = lookupIds(self._db, prop,
337 handleListCGIValue(form[item]))
338 elif isinstance(prop, hyperdb.Link):
339 value = form[item].value.strip()
340 if value:
341 value = lookupIds(self._db, prop, [value])[0]
342 else:
343 value = None
344 else:
345 value = form[item].value.strip() or None
346 else:
347 if isinstance(prop, hyperdb.Multilink):
348 value = []
349 else:
350 value = None
351 return htmlklass(self._client, self._classname, '', prop, item,
352 value, self._anonymous)
354 # no good
355 raise KeyError, item
357 def __getattr__(self, attr):
358 ''' convenience access '''
359 try:
360 return self[attr]
361 except KeyError:
362 raise AttributeError, attr
364 def designator(self):
365 ''' Return this class' designator (classname) '''
366 return self._classname
368 def getItem(self, itemid, num_re=re.compile('-?\d+')):
369 ''' Get an item of this class by its item id.
370 '''
371 # make sure we're looking at an itemid
372 if not num_re.match(itemid):
373 itemid = self._klass.lookup(itemid)
375 if self.classname == 'user':
376 klass = HTMLUser
377 else:
378 klass = HTMLItem
380 return klass(self._client, self.classname, itemid)
382 def properties(self, sort=1):
383 ''' Return HTMLProperty for all of this class' properties.
384 '''
385 l = []
386 for name, prop in self._props.items():
387 for klass, htmlklass in propclasses:
388 if isinstance(prop, hyperdb.Multilink):
389 value = []
390 else:
391 value = None
392 if isinstance(prop, klass):
393 l.append(htmlklass(self._client, self._classname, '',
394 prop, name, value, self._anonymous))
395 if sort:
396 l.sort(lambda a,b:cmp(a._name, b._name))
397 return l
399 def list(self):
400 ''' List all items in this class.
401 '''
402 if self.classname == 'user':
403 klass = HTMLUser
404 else:
405 klass = HTMLItem
407 # get the list and sort it nicely
408 l = self._klass.list()
409 sortfunc = make_sort_function(self._db, self.classname)
410 l.sort(sortfunc)
412 l = [klass(self._client, self.classname, x) for x in l]
413 return l
415 def csv(self):
416 ''' Return the items of this class as a chunk of CSV text.
417 '''
418 if rcsv.error:
419 return rcsv.error
421 props = self.propnames()
422 s = StringIO.StringIO()
423 writer = rcsv.writer(s, rcsv.comma_separated)
424 writer.writerow(props)
425 for nodeid in self._klass.list():
426 l = []
427 for name in props:
428 value = self._klass.get(nodeid, name)
429 if value is None:
430 l.append('')
431 elif isinstance(value, type([])):
432 l.append(':'.join(map(str, value)))
433 else:
434 l.append(str(self._klass.get(nodeid, name)))
435 writer.writerow(l)
436 return s.getvalue()
438 def propnames(self):
439 ''' Return the list of the names of the properties of this class.
440 '''
441 idlessprops = self._klass.getprops(protected=0).keys()
442 idlessprops.sort()
443 return ['id'] + idlessprops
445 def filter(self, request=None):
446 ''' Return a list of items from this class, filtered and sorted
447 by the current requested filterspec/filter/sort/group args
448 '''
449 # XXX allow direct specification of the filterspec etc.
450 if request is not None:
451 filterspec = request.filterspec
452 sort = request.sort
453 group = request.group
454 else:
455 filterspec = {}
456 sort = (None,None)
457 group = (None,None)
458 if self.classname == 'user':
459 klass = HTMLUser
460 else:
461 klass = HTMLItem
462 l = [klass(self._client, self.classname, x)
463 for x in self._klass.filter(None, filterspec, sort, group)]
464 return l
466 def classhelp(self, properties=None, label='(list)', width='500',
467 height='400', property=''):
468 ''' Pop up a javascript window with class help
470 This generates a link to a popup window which displays the
471 properties indicated by "properties" of the class named by
472 "classname". The "properties" should be a comma-separated list
473 (eg. 'id,name,description'). Properties defaults to all the
474 properties of a class (excluding id, creator, created and
475 activity).
477 You may optionally override the label displayed, the width and
478 height. The popup window will be resizable and scrollable.
480 If the "property" arg is given, it's passed through to the
481 javascript help_window function.
482 '''
483 if properties is None:
484 properties = self._klass.getprops(protected=0).keys()
485 properties.sort()
486 properties = ','.join(properties)
487 if property:
488 property = '&property=%s'%property
489 return '<a class="classhelp" href="javascript:help_window(\'%s?'\
490 '@startwith=0&@template=help&properties=%s%s\', \'%s\', \
491 \'%s\')">%s</a>'%(self.classname, properties, property, width,
492 height, label)
494 def submit(self, label="Submit New Entry"):
495 ''' Generate a submit button (and action hidden element)
496 '''
497 return self.input(type="hidden",name="@action",value="new") + '\n' + \
498 self.input(type="submit",name="submit",value=label)
500 def history(self):
501 return 'New node - no history'
503 def renderWith(self, name, **kwargs):
504 ''' Render this class with the given template.
505 '''
506 # create a new request and override the specified args
507 req = HTMLRequest(self._client)
508 req.classname = self.classname
509 req.update(kwargs)
511 # new template, using the specified classname and request
512 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
514 # use our fabricated request
515 return pt.render(self._client, self.classname, req)
517 class HTMLItem(HTMLPermissions):
518 ''' Accesses through an *item*
519 '''
520 def __init__(self, client, classname, nodeid, anonymous=0):
521 self._client = client
522 self._db = client.db
523 self._classname = classname
524 self._nodeid = nodeid
525 self._klass = self._db.getclass(classname)
526 self._props = self._klass.getprops()
528 # do we prefix the form items with the item's identification?
529 self._anonymous = anonymous
531 def __repr__(self):
532 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
533 self._nodeid)
535 def __getitem__(self, item):
536 ''' return an HTMLProperty instance
537 '''
538 #print 'HTMLItem.getitem', (self, item)
539 if item == 'id':
540 return self._nodeid
542 # get the property
543 prop = self._props[item]
545 # get the value, handling missing values
546 value = None
547 if int(self._nodeid) > 0:
548 value = self._klass.get(self._nodeid, item, None)
549 if value is None:
550 if isinstance(self._props[item], hyperdb.Multilink):
551 value = []
553 # look up the correct HTMLProperty class
554 for klass, htmlklass in propclasses:
555 if isinstance(prop, klass):
556 return htmlklass(self._client, self._classname,
557 self._nodeid, prop, item, value, self._anonymous)
559 raise KeyError, item
561 def __getattr__(self, attr):
562 ''' convenience access to properties '''
563 try:
564 return self[attr]
565 except KeyError:
566 raise AttributeError, attr
568 def designator(self):
569 ''' Return this item's designator (classname + id) '''
570 return '%s%s'%(self._classname, self._nodeid)
572 def submit(self, label="Submit Changes"):
573 ''' Generate a submit button (and action hidden element)
574 '''
575 return self.input(type="hidden",name="@action",value="edit") + '\n' + \
576 self.input(type="submit",name="submit",value=label)
578 def journal(self, direction='descending'):
579 ''' Return a list of HTMLJournalEntry instances.
580 '''
581 # XXX do this
582 return []
584 def history(self, direction='descending', dre=re.compile('\d+')):
585 l = ['<table class="history">'
586 '<tr><th colspan="4" class="header">',
587 _('History'),
588 '</th></tr><tr>',
589 _('<th>Date</th>'),
590 _('<th>User</th>'),
591 _('<th>Action</th>'),
592 _('<th>Args</th>'),
593 '</tr>']
594 current = {}
595 comments = {}
596 history = self._klass.history(self._nodeid)
597 history.sort()
598 timezone = self._db.getUserTimezone()
599 if direction == 'descending':
600 history.reverse()
601 for prop_n in self._props.keys():
602 prop = self[prop_n]
603 if isinstance(prop, HTMLProperty):
604 current[prop_n] = prop.plain()
605 # make link if hrefable
606 if (self._props.has_key(prop_n) and
607 isinstance(self._props[prop_n], hyperdb.Link)):
608 classname = self._props[prop_n].classname
609 try:
610 template = find_template(self._db.config.TEMPLATES,
611 classname, 'item')
612 if template[1].startswith('_generic'):
613 raise NoTemplate, 'not really...'
614 except NoTemplate:
615 pass
616 else:
617 id = self._klass.get(self._nodeid, prop_n, None)
618 current[prop_n] = '<a href="%s%s">%s</a>'%(
619 classname, id, current[prop_n])
621 for id, evt_date, user, action, args in history:
622 date_s = str(evt_date.local(timezone)).replace("."," ")
623 arg_s = ''
624 if action == 'link' and type(args) == type(()):
625 if len(args) == 3:
626 linkcl, linkid, key = args
627 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
628 linkcl, linkid, key)
629 else:
630 arg_s = str(args)
632 elif action == 'unlink' and type(args) == type(()):
633 if len(args) == 3:
634 linkcl, linkid, key = args
635 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
636 linkcl, linkid, key)
637 else:
638 arg_s = str(args)
640 elif type(args) == type({}):
641 cell = []
642 for k in args.keys():
643 # try to get the relevant property and treat it
644 # specially
645 try:
646 prop = self._props[k]
647 except KeyError:
648 prop = None
649 if prop is None:
650 # property no longer exists
651 comments['no_exist'] = _('''<em>The indicated property
652 no longer exists</em>''')
653 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
654 continue
656 if args[k] and (isinstance(prop, hyperdb.Multilink) or
657 isinstance(prop, hyperdb.Link)):
658 # figure what the link class is
659 classname = prop.classname
660 try:
661 linkcl = self._db.getclass(classname)
662 except KeyError:
663 labelprop = None
664 comments[classname] = _('''The linked class
665 %(classname)s no longer exists''')%locals()
666 labelprop = linkcl.labelprop(1)
667 try:
668 template = find_template(self._db.config.TEMPLATES,
669 classname, 'item')
670 if template[1].startswith('_generic'):
671 raise NoTemplate, 'not really...'
672 hrefable = 1
673 except NoTemplate:
674 hrefable = 0
676 if isinstance(prop, hyperdb.Multilink) and args[k]:
677 ml = []
678 for linkid in args[k]:
679 if isinstance(linkid, type(())):
680 sublabel = linkid[0] + ' '
681 linkids = linkid[1]
682 else:
683 sublabel = ''
684 linkids = [linkid]
685 subml = []
686 for linkid in linkids:
687 label = classname + linkid
688 # if we have a label property, try to use it
689 # TODO: test for node existence even when
690 # there's no labelprop!
691 try:
692 if labelprop is not None and \
693 labelprop != 'id':
694 label = linkcl.get(linkid, labelprop)
695 except IndexError:
696 comments['no_link'] = _('''<strike>The
697 linked node no longer
698 exists</strike>''')
699 subml.append('<strike>%s</strike>'%label)
700 else:
701 if hrefable:
702 subml.append('<a href="%s%s">%s</a>'%(
703 classname, linkid, label))
704 else:
705 subml.append(label)
706 ml.append(sublabel + ', '.join(subml))
707 cell.append('%s:\n %s'%(k, ', '.join(ml)))
708 elif isinstance(prop, hyperdb.Link) and args[k]:
709 label = classname + args[k]
710 # if we have a label property, try to use it
711 # TODO: test for node existence even when
712 # there's no labelprop!
713 if labelprop is not None and labelprop != 'id':
714 try:
715 label = linkcl.get(args[k], labelprop)
716 except IndexError:
717 comments['no_link'] = _('''<strike>The
718 linked node no longer
719 exists</strike>''')
720 cell.append(' <strike>%s</strike>,\n'%label)
721 # "flag" this is done .... euwww
722 label = None
723 if label is not None:
724 if hrefable:
725 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
726 else:
727 old = label;
728 cell.append('%s: %s' % (k,old))
729 if current.has_key(k):
730 cell[-1] += ' -> %s'%current[k]
731 current[k] = old
733 elif isinstance(prop, hyperdb.Date) and args[k]:
734 d = date.Date(args[k]).local(timezone)
735 cell.append('%s: %s'%(k, str(d)))
736 if current.has_key(k):
737 cell[-1] += ' -> %s' % current[k]
738 current[k] = str(d)
740 elif isinstance(prop, hyperdb.Interval) and args[k]:
741 d = date.Interval(args[k])
742 cell.append('%s: %s'%(k, str(d)))
743 if current.has_key(k):
744 cell[-1] += ' -> %s'%current[k]
745 current[k] = str(d)
747 elif isinstance(prop, hyperdb.String) and args[k]:
748 cell.append('%s: %s'%(k, cgi.escape(args[k])))
749 if current.has_key(k):
750 cell[-1] += ' -> %s'%current[k]
751 current[k] = cgi.escape(args[k])
753 elif not args[k]:
754 if current.has_key(k):
755 cell.append('%s: %s'%(k, current[k]))
756 current[k] = '(no value)'
757 else:
758 cell.append('%s: (no value)'%k)
760 else:
761 cell.append('%s: %s'%(k, str(args[k])))
762 if current.has_key(k):
763 cell[-1] += ' -> %s'%current[k]
764 current[k] = str(args[k])
766 arg_s = '<br />'.join(cell)
767 else:
768 # unkown event!!
769 comments['unknown'] = _('''<strong><em>This event is not
770 handled by the history display!</em></strong>''')
771 arg_s = '<strong><em>' + str(args) + '</em></strong>'
772 date_s = date_s.replace(' ', ' ')
773 # if the user's an itemid, figure the username (older journals
774 # have the username)
775 if dre.match(user):
776 user = self._db.user.get(user, 'username')
777 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
778 date_s, user, action, arg_s))
779 if comments:
780 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
781 for entry in comments.values():
782 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
783 l.append('</table>')
784 return '\n'.join(l)
786 def renderQueryForm(self):
787 ''' Render this item, which is a query, as a search form.
788 '''
789 # create a new request and override the specified args
790 req = HTMLRequest(self._client)
791 req.classname = self._klass.get(self._nodeid, 'klass')
792 name = self._klass.get(self._nodeid, 'name')
793 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
794 '&@queryname=%s'%urllib.quote(name))
796 # new template, using the specified classname and request
797 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
799 # use our fabricated request
800 return pt.render(self._client, req.classname, req)
802 class HTMLUser(HTMLItem):
803 ''' Accesses through the *user* (a special case of item)
804 '''
805 def __init__(self, client, classname, nodeid, anonymous=0):
806 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
807 self._default_classname = client.classname
809 # used for security checks
810 self._security = client.db.security
812 _marker = []
813 def hasPermission(self, permission, classname=_marker):
814 ''' Determine if the user has the Permission.
816 The class being tested defaults to the template's class, but may
817 be overidden for this test by suppling an alternate classname.
818 '''
819 if classname is self._marker:
820 classname = self._default_classname
821 return self._security.hasPermission(permission, self._nodeid, classname)
823 def is_edit_ok(self):
824 ''' Is the user allowed to Edit the current class?
825 Also check whether this is the current user's info.
826 '''
827 return self._db.security.hasPermission('Edit', self._client.userid,
828 self._classname) or (self._nodeid == self._client.userid and
829 self._db.user.get(self._client.userid, 'username') != 'anonymous')
831 def is_view_ok(self):
832 ''' Is the user allowed to View the current class?
833 Also check whether this is the current user's info.
834 '''
835 return self._db.security.hasPermission('Edit', self._client.userid,
836 self._classname) or (self._nodeid == self._client.userid and
837 self._db.user.get(self._client.userid, 'username') != 'anonymous')
839 class HTMLProperty:
840 ''' String, Number, Date, Interval HTMLProperty
842 Has useful attributes:
844 _name the name of the property
845 _value the value of the property if any
847 A wrapper object which may be stringified for the plain() behaviour.
848 '''
849 def __init__(self, client, classname, nodeid, prop, name, value,
850 anonymous=0):
851 self._client = client
852 self._db = client.db
853 self._classname = classname
854 self._nodeid = nodeid
855 self._prop = prop
856 self._value = value
857 self._anonymous = anonymous
858 self._name = name
859 if not anonymous:
860 self._formname = '%s%s@%s'%(classname, nodeid, name)
861 else:
862 self._formname = name
864 html_version = 'html4'
865 if hasattr(self._client.instance.config, 'HTML_VERSION'):
866 html_version = self._client.instance.config.HTML_VERSION
867 if html_version == 'xhtml':
868 self.input = input_xhtml
869 else:
870 self.input = input_html4
872 def __repr__(self):
873 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
874 self._prop, self._value)
875 def __str__(self):
876 return self.plain()
877 def __cmp__(self, other):
878 if isinstance(other, HTMLProperty):
879 return cmp(self._value, other._value)
880 return cmp(self._value, other)
882 class StringHTMLProperty(HTMLProperty):
883 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
884 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
885 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
886 def _hyper_repl(self, match):
887 if match.group('url'):
888 s = match.group('url')
889 return '<a href="%s">%s</a>'%(s, s)
890 elif match.group('email'):
891 s = match.group('email')
892 return '<a href="mailto:%s">%s</a>'%(s, s)
893 else:
894 s = match.group('item')
895 s1 = match.group('class')
896 s2 = match.group('id')
897 try:
898 # make sure s1 is a valid tracker classname
899 self._db.getclass(s1)
900 return '<a href="%s">%s %s</a>'%(s, s1, s2)
901 except KeyError:
902 return '%s%s'%(s1, s2)
904 def hyperlinked(self):
905 ''' Render a "hyperlinked" version of the text '''
906 return self.plain(hyperlink=1)
908 def plain(self, escape=0, hyperlink=0):
909 ''' Render a "plain" representation of the property
911 "escape" turns on/off HTML quoting
912 "hyperlink" turns on/off in-text hyperlinking of URLs, email
913 addresses and designators
914 '''
915 if self._value is None:
916 return ''
917 if escape:
918 s = cgi.escape(str(self._value))
919 else:
920 s = str(self._value)
921 if hyperlink:
922 # no, we *must* escape this text
923 if not escape:
924 s = cgi.escape(s)
925 s = self.hyper_re.sub(self._hyper_repl, s)
926 return s
928 def stext(self, escape=0):
929 ''' Render the value of the property as StructuredText.
931 This requires the StructureText module to be installed separately.
932 '''
933 s = self.plain(escape=escape)
934 if not StructuredText:
935 return s
936 return StructuredText(s,level=1,header=0)
938 def field(self, size = 30):
939 ''' Render a form edit field for the property
940 '''
941 if self._value is None:
942 value = ''
943 else:
944 value = cgi.escape(str(self._value))
945 value = '"'.join(value.split('"'))
946 return self.input(name=self._formname,value=value,size=size)
948 def multiline(self, escape=0, rows=5, cols=40):
949 ''' Render a multiline form edit field for the property
950 '''
951 if self._value is None:
952 value = ''
953 else:
954 value = cgi.escape(str(self._value))
955 value = '"'.join(value.split('"'))
956 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
957 self._formname, rows, cols, value)
959 def email(self, escape=1):
960 ''' Render the value of the property as an obscured email address
961 '''
962 if self._value is None: value = ''
963 else: value = str(self._value)
964 if value.find('@') != -1:
965 name, domain = value.split('@')
966 domain = ' '.join(domain.split('.')[:-1])
967 name = name.replace('.', ' ')
968 value = '%s at %s ...'%(name, domain)
969 else:
970 value = value.replace('.', ' ')
971 if escape:
972 value = cgi.escape(value)
973 return value
975 class PasswordHTMLProperty(HTMLProperty):
976 def plain(self):
977 ''' Render a "plain" representation of the property
978 '''
979 if self._value is None:
980 return ''
981 return _('*encrypted*')
983 def field(self, size = 30):
984 ''' Render a form edit field for the property.
985 '''
986 return self.input(type="password", name=self._formname, size=size)
988 def confirm(self, size = 30):
989 ''' Render a second form edit field for the property, used for
990 confirmation that the user typed the password correctly. Generates
991 a field with name "@confirm@name".
992 '''
993 return self.input(type="password", name="@confirm@%s"%self._formname,
994 size=size)
996 class NumberHTMLProperty(HTMLProperty):
997 def plain(self):
998 ''' Render a "plain" representation of the property
999 '''
1000 return str(self._value)
1002 def field(self, size = 30):
1003 ''' Render a form edit field for the property
1004 '''
1005 if self._value is None:
1006 value = ''
1007 else:
1008 value = cgi.escape(str(self._value))
1009 value = '"'.join(value.split('"'))
1010 return self.input(name=self._formname,value=value,size=size)
1012 def __int__(self):
1013 ''' Return an int of me
1014 '''
1015 return int(self._value)
1017 def __float__(self):
1018 ''' Return a float of me
1019 '''
1020 return float(self._value)
1023 class BooleanHTMLProperty(HTMLProperty):
1024 def plain(self):
1025 ''' Render a "plain" representation of the property
1026 '''
1027 if self._value is None:
1028 return ''
1029 return self._value and "Yes" or "No"
1031 def field(self):
1032 ''' Render a form edit field for the property
1033 '''
1034 checked = self._value and "checked" or ""
1035 if self._value:
1036 s = self.input(type="radio",name=self._formname,value="yes",checked="checked")
1037 s += 'Yes'
1038 s +=self.input(type="radio",name=self._formname,value="no")
1039 s += 'No'
1040 else:
1041 s = self.input(type="radio",name=self._formname,value="yes")
1042 s += 'Yes'
1043 s +=self.input(type="radio",name=self._formname,value="no",checked="checked")
1044 s += 'No'
1045 return s
1047 class DateHTMLProperty(HTMLProperty):
1048 def plain(self):
1049 ''' Render a "plain" representation of the property
1050 '''
1051 if self._value is None:
1052 return ''
1053 return str(self._value.local(self._db.getUserTimezone()))
1055 def now(self):
1056 ''' Return the current time.
1058 This is useful for defaulting a new value. Returns a
1059 DateHTMLProperty.
1060 '''
1061 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1062 self._formname, date.Date('.'))
1064 def field(self, size = 30):
1065 ''' Render a form edit field for the property
1066 '''
1067 if self._value is None:
1068 value = ''
1069 else:
1070 value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1071 value = '"'.join(value.split('"'))
1072 return self.input(name=self._formname,value=value,size=size)
1074 def reldate(self, pretty=1):
1075 ''' Render the interval between the date and now.
1077 If the "pretty" flag is true, then make the display pretty.
1078 '''
1079 if not self._value:
1080 return ''
1082 # figure the interval
1083 interval = self._value - date.Date('.')
1084 if pretty:
1085 return interval.pretty()
1086 return str(interval)
1088 _marker = []
1089 def pretty(self, format=_marker):
1090 ''' Render the date in a pretty format (eg. month names, spaces).
1092 The format string is a standard python strftime format string.
1093 Note that if the day is zero, and appears at the start of the
1094 string, then it'll be stripped from the output. This is handy
1095 for the situatin when a date only specifies a month and a year.
1096 '''
1097 if format is not self._marker:
1098 return self._value.pretty(format)
1099 else:
1100 return self._value.pretty()
1102 def local(self, offset):
1103 ''' Return the date/time as a local (timezone offset) date/time.
1104 '''
1105 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1106 self._formname, self._value.local(offset))
1108 class IntervalHTMLProperty(HTMLProperty):
1109 def plain(self):
1110 ''' Render a "plain" representation of the property
1111 '''
1112 if self._value is None:
1113 return ''
1114 return str(self._value)
1116 def pretty(self):
1117 ''' Render the interval in a pretty format (eg. "yesterday")
1118 '''
1119 return self._value.pretty()
1121 def field(self, size = 30):
1122 ''' Render a form edit field for the property
1123 '''
1124 if self._value is None:
1125 value = ''
1126 else:
1127 value = cgi.escape(str(self._value))
1128 value = '"'.join(value.split('"'))
1129 return self.input(name=self._formname,value=value,size=size)
1131 class LinkHTMLProperty(HTMLProperty):
1132 ''' Link HTMLProperty
1133 Include the above as well as being able to access the class
1134 information. Stringifying the object itself results in the value
1135 from the item being displayed. Accessing attributes of this object
1136 result in the appropriate entry from the class being queried for the
1137 property accessed (so item/assignedto/name would look up the user
1138 entry identified by the assignedto property on item, and then the
1139 name property of that user)
1140 '''
1141 def __init__(self, *args, **kw):
1142 HTMLProperty.__init__(self, *args, **kw)
1143 # if we're representing a form value, then the -1 from the form really
1144 # should be a None
1145 if str(self._value) == '-1':
1146 self._value = None
1148 def __getattr__(self, attr):
1149 ''' return a new HTMLItem '''
1150 #print 'Link.getattr', (self, attr, self._value)
1151 if not self._value:
1152 raise AttributeError, "Can't access missing value"
1153 if self._prop.classname == 'user':
1154 klass = HTMLUser
1155 else:
1156 klass = HTMLItem
1157 i = klass(self._client, self._prop.classname, self._value)
1158 return getattr(i, attr)
1160 def plain(self, escape=0):
1161 ''' Render a "plain" representation of the property
1162 '''
1163 if self._value is None:
1164 return ''
1165 linkcl = self._db.classes[self._prop.classname]
1166 k = linkcl.labelprop(1)
1167 value = str(linkcl.get(self._value, k))
1168 if escape:
1169 value = cgi.escape(value)
1170 return value
1172 def field(self, showid=0, size=None):
1173 ''' Render a form edit field for the property
1174 '''
1175 linkcl = self._db.getclass(self._prop.classname)
1176 if linkcl.getprops().has_key('order'):
1177 sort_on = 'order'
1178 else:
1179 sort_on = linkcl.labelprop()
1180 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1181 # TODO: make this a field display, not a menu one!
1182 l = ['<select name="%s">'%self._formname]
1183 k = linkcl.labelprop(1)
1184 if self._value is None:
1185 s = 'selected="selected" '
1186 else:
1187 s = ''
1188 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1190 # make sure we list the current value if it's retired
1191 if self._value and self._value not in options:
1192 options.insert(0, self._value)
1194 for optionid in options:
1195 # get the option value, and if it's None use an empty string
1196 option = linkcl.get(optionid, k) or ''
1198 # figure if this option is selected
1199 s = ''
1200 if optionid == self._value:
1201 s = 'selected="selected" '
1203 # figure the label
1204 if showid:
1205 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1206 else:
1207 lab = option
1209 # truncate if it's too long
1210 if size is not None and len(lab) > size:
1211 lab = lab[:size-3] + '...'
1213 # and generate
1214 lab = cgi.escape(lab)
1215 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1216 l.append('</select>')
1217 return '\n'.join(l)
1219 def menu(self, size=None, height=None, showid=0, additional=[],
1220 **conditions):
1221 ''' Render a form select list for this property
1222 '''
1223 value = self._value
1225 linkcl = self._db.getclass(self._prop.classname)
1226 l = ['<select name="%s">'%self._formname]
1227 k = linkcl.labelprop(1)
1228 s = ''
1229 if value is None:
1230 s = 'selected="selected" '
1231 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1232 if linkcl.getprops().has_key('order'):
1233 sort_on = ('+', 'order')
1234 else:
1235 sort_on = ('+', linkcl.labelprop())
1236 options = linkcl.filter(None, conditions, sort_on, (None, None))
1238 # make sure we list the current value if it's retired
1239 if self._value and self._value not in options:
1240 options.insert(0, self._value)
1242 for optionid in options:
1243 # get the option value, and if it's None use an empty string
1244 option = linkcl.get(optionid, k) or ''
1246 # figure if this option is selected
1247 s = ''
1248 if value in [optionid, option]:
1249 s = 'selected="selected" '
1251 # figure the label
1252 if showid:
1253 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1254 else:
1255 lab = option
1257 # truncate if it's too long
1258 if size is not None and len(lab) > size:
1259 lab = lab[:size-3] + '...'
1260 if additional:
1261 m = []
1262 for propname in additional:
1263 m.append(linkcl.get(optionid, propname))
1264 lab = lab + ' (%s)'%', '.join(map(str, m))
1266 # and generate
1267 lab = cgi.escape(lab)
1268 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1269 l.append('</select>')
1270 return '\n'.join(l)
1271 # def checklist(self, ...)
1273 class MultilinkHTMLProperty(HTMLProperty):
1274 ''' Multilink HTMLProperty
1276 Also be iterable, returning a wrapper object like the Link case for
1277 each entry in the multilink.
1278 '''
1279 def __init__(self, *args, **kwargs):
1280 HTMLProperty.__init__(self, *args, **kwargs)
1281 if self._value:
1282 self._value.sort(make_sort_function(self._db, self._prop.classname))
1284 def __len__(self):
1285 ''' length of the multilink '''
1286 return len(self._value)
1288 def __getattr__(self, attr):
1289 ''' no extended attribute accesses make sense here '''
1290 raise AttributeError, attr
1292 def __getitem__(self, num):
1293 ''' iterate and return a new HTMLItem
1294 '''
1295 #print 'Multi.getitem', (self, num)
1296 value = self._value[num]
1297 if self._prop.classname == 'user':
1298 klass = HTMLUser
1299 else:
1300 klass = HTMLItem
1301 return klass(self._client, self._prop.classname, value)
1303 def __contains__(self, value):
1304 ''' Support the "in" operator. We have to make sure the passed-in
1305 value is a string first, not a *HTMLProperty.
1306 '''
1307 return str(value) in self._value
1309 def reverse(self):
1310 ''' return the list in reverse order
1311 '''
1312 l = self._value[:]
1313 l.reverse()
1314 if self._prop.classname == 'user':
1315 klass = HTMLUser
1316 else:
1317 klass = HTMLItem
1318 return [klass(self._client, self._prop.classname, value) for value in l]
1320 def plain(self, escape=0):
1321 ''' Render a "plain" representation of the property
1322 '''
1323 linkcl = self._db.classes[self._prop.classname]
1324 k = linkcl.labelprop(1)
1325 labels = []
1326 for v in self._value:
1327 labels.append(linkcl.get(v, k))
1328 value = ', '.join(labels)
1329 if escape:
1330 value = cgi.escape(value)
1331 return value
1333 def field(self, size=30, showid=0):
1334 ''' Render a form edit field for the property
1335 '''
1336 linkcl = self._db.getclass(self._prop.classname)
1337 value = self._value[:]
1338 # map the id to the label property
1339 if not linkcl.getkey():
1340 showid=1
1341 if not showid:
1342 k = linkcl.labelprop(1)
1343 value = [linkcl.get(v, k) for v in value]
1344 value = cgi.escape(','.join(value))
1345 return self.input(name=self._formname,size=size,value=value)
1347 def menu(self, size=None, height=None, showid=0, additional=[],
1348 **conditions):
1349 ''' Render a form select list for this property
1350 '''
1351 value = self._value
1353 linkcl = self._db.getclass(self._prop.classname)
1354 sort_on = ('+', find_sort_key(linkcl))
1355 options = linkcl.filter(None, conditions, sort_on)
1356 height = height or min(len(options), 7)
1357 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1358 k = linkcl.labelprop(1)
1360 # make sure we list the current values if they're retired
1361 for val in value:
1362 if val not in options:
1363 options.insert(0, val)
1365 for optionid in options:
1366 # get the option value, and if it's None use an empty string
1367 option = linkcl.get(optionid, k) or ''
1369 # figure if this option is selected
1370 s = ''
1371 if optionid in value or option in value:
1372 s = 'selected="selected" '
1374 # figure the label
1375 if showid:
1376 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1377 else:
1378 lab = option
1379 # truncate if it's too long
1380 if size is not None and len(lab) > size:
1381 lab = lab[:size-3] + '...'
1382 if additional:
1383 m = []
1384 for propname in additional:
1385 m.append(linkcl.get(optionid, propname))
1386 lab = lab + ' (%s)'%', '.join(m)
1388 # and generate
1389 lab = cgi.escape(lab)
1390 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1391 lab))
1392 l.append('</select>')
1393 return '\n'.join(l)
1395 # set the propclasses for HTMLItem
1396 propclasses = (
1397 (hyperdb.String, StringHTMLProperty),
1398 (hyperdb.Number, NumberHTMLProperty),
1399 (hyperdb.Boolean, BooleanHTMLProperty),
1400 (hyperdb.Date, DateHTMLProperty),
1401 (hyperdb.Interval, IntervalHTMLProperty),
1402 (hyperdb.Password, PasswordHTMLProperty),
1403 (hyperdb.Link, LinkHTMLProperty),
1404 (hyperdb.Multilink, MultilinkHTMLProperty),
1405 )
1407 def make_sort_function(db, classname):
1408 '''Make a sort function for a given class
1409 '''
1410 linkcl = db.getclass(classname)
1411 sort_on = find_sort_key(linkcl)
1412 def sortfunc(a, b):
1413 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1414 return sortfunc
1416 def find_sort_key(linkcl):
1417 if linkcl.getprops().has_key('order'):
1418 return 'order'
1419 else:
1420 return linkcl.labelprop()
1422 def handleListCGIValue(value):
1423 ''' Value is either a single item or a list of items. Each item has a
1424 .value that we're actually interested in.
1425 '''
1426 if isinstance(value, type([])):
1427 return [value.value for value in value]
1428 else:
1429 value = value.value.strip()
1430 if not value:
1431 return []
1432 return value.split(',')
1434 class ShowDict:
1435 ''' A convenience access to the :columns index parameters
1436 '''
1437 def __init__(self, columns):
1438 self.columns = {}
1439 for col in columns:
1440 self.columns[col] = 1
1441 def __getitem__(self, name):
1442 return self.columns.has_key(name)
1444 class HTMLRequest:
1445 ''' The *request*, holding the CGI form and environment.
1447 "form" the CGI form as a cgi.FieldStorage
1448 "env" the CGI environment variables
1449 "base" the base URL for this instance
1450 "user" a HTMLUser instance for this user
1451 "classname" the current classname (possibly None)
1452 "template" the current template (suffix, also possibly None)
1454 Index args:
1455 "columns" dictionary of the columns to display in an index page
1456 "show" a convenience access to columns - request/show/colname will
1457 be true if the columns should be displayed, false otherwise
1458 "sort" index sort column (direction, column name)
1459 "group" index grouping property (direction, column name)
1460 "filter" properties to filter the index on
1461 "filterspec" values to filter the index on
1462 "search_text" text to perform a full-text search on for an index
1464 '''
1465 def __init__(self, client):
1466 self.client = client
1468 # easier access vars
1469 self.form = client.form
1470 self.env = client.env
1471 self.base = client.base
1472 self.user = HTMLUser(client, 'user', client.userid)
1474 # store the current class name and action
1475 self.classname = client.classname
1476 self.template = client.template
1478 # the special char to use for special vars
1479 self.special_char = '@'
1481 html_version = 'html4'
1482 if hasattr(self.client.instance.config, 'HTML_VERSION'):
1483 html_version = self.client.instance.config.HTML_VERSION
1484 if html_version == 'xhtml':
1485 self.input = input_xhtml
1486 else:
1487 self.input = input_html4
1489 self._post_init()
1491 def _post_init(self):
1492 ''' Set attributes based on self.form
1493 '''
1494 # extract the index display information from the form
1495 self.columns = []
1496 for name in ':columns @columns'.split():
1497 if self.form.has_key(name):
1498 self.special_char = name[0]
1499 self.columns = handleListCGIValue(self.form[name])
1500 break
1501 self.show = ShowDict(self.columns)
1503 # sorting
1504 self.sort = (None, None)
1505 for name in ':sort @sort'.split():
1506 if self.form.has_key(name):
1507 self.special_char = name[0]
1508 sort = self.form[name].value
1509 if sort.startswith('-'):
1510 self.sort = ('-', sort[1:])
1511 else:
1512 self.sort = ('+', sort)
1513 if self.form.has_key(self.special_char+'sortdir'):
1514 self.sort = ('-', self.sort[1])
1516 # grouping
1517 self.group = (None, None)
1518 for name in ':group @group'.split():
1519 if self.form.has_key(name):
1520 self.special_char = name[0]
1521 group = self.form[name].value
1522 if group.startswith('-'):
1523 self.group = ('-', group[1:])
1524 else:
1525 self.group = ('+', group)
1526 if self.form.has_key(self.special_char+'groupdir'):
1527 self.group = ('-', self.group[1])
1529 # filtering
1530 self.filter = []
1531 for name in ':filter @filter'.split():
1532 if self.form.has_key(name):
1533 self.special_char = name[0]
1534 self.filter = handleListCGIValue(self.form[name])
1536 self.filterspec = {}
1537 db = self.client.db
1538 if self.classname is not None:
1539 props = db.getclass(self.classname).getprops()
1540 for name in self.filter:
1541 if not self.form.has_key(name):
1542 continue
1543 prop = props[name]
1544 fv = self.form[name]
1545 if (isinstance(prop, hyperdb.Link) or
1546 isinstance(prop, hyperdb.Multilink)):
1547 self.filterspec[name] = lookupIds(db, prop,
1548 handleListCGIValue(fv))
1549 else:
1550 if isinstance(fv, type([])):
1551 self.filterspec[name] = [v.value for v in fv]
1552 else:
1553 self.filterspec[name] = fv.value
1555 # full-text search argument
1556 self.search_text = None
1557 for name in ':search_text @search_text'.split():
1558 if self.form.has_key(name):
1559 self.special_char = name[0]
1560 self.search_text = self.form[name].value
1562 # pagination - size and start index
1563 # figure batch args
1564 self.pagesize = 50
1565 for name in ':pagesize @pagesize'.split():
1566 if self.form.has_key(name):
1567 self.special_char = name[0]
1568 self.pagesize = int(self.form[name].value)
1570 self.startwith = 0
1571 for name in ':startwith @startwith'.split():
1572 if self.form.has_key(name):
1573 self.special_char = name[0]
1574 self.startwith = int(self.form[name].value)
1576 def updateFromURL(self, url):
1577 ''' Parse the URL for query args, and update my attributes using the
1578 values.
1579 '''
1580 env = {'QUERY_STRING': url}
1581 self.form = cgi.FieldStorage(environ=env)
1583 self._post_init()
1585 def update(self, kwargs):
1586 ''' Update my attributes using the keyword args
1587 '''
1588 self.__dict__.update(kwargs)
1589 if kwargs.has_key('columns'):
1590 self.show = ShowDict(self.columns)
1592 def description(self):
1593 ''' Return a description of the request - handle for the page title.
1594 '''
1595 s = [self.client.db.config.TRACKER_NAME]
1596 if self.classname:
1597 if self.client.nodeid:
1598 s.append('- %s%s'%(self.classname, self.client.nodeid))
1599 else:
1600 if self.template == 'item':
1601 s.append('- new %s'%self.classname)
1602 elif self.template == 'index':
1603 s.append('- %s index'%self.classname)
1604 else:
1605 s.append('- %s %s'%(self.classname, self.template))
1606 else:
1607 s.append('- home')
1608 return ' '.join(s)
1610 def __str__(self):
1611 d = {}
1612 d.update(self.__dict__)
1613 f = ''
1614 for k in self.form.keys():
1615 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1616 d['form'] = f
1617 e = ''
1618 for k,v in self.env.items():
1619 e += '\n %r=%r'%(k, v)
1620 d['env'] = e
1621 return '''
1622 form: %(form)s
1623 base: %(base)r
1624 classname: %(classname)r
1625 template: %(template)r
1626 columns: %(columns)r
1627 sort: %(sort)r
1628 group: %(group)r
1629 filter: %(filter)r
1630 search_text: %(search_text)r
1631 pagesize: %(pagesize)r
1632 startwith: %(startwith)r
1633 env: %(env)s
1634 '''%d
1636 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1637 filterspec=1):
1638 ''' return the current index args as form elements '''
1639 l = []
1640 sc = self.special_char
1641 s = self.input(type="hidden",name="%s",value="%s")
1642 if columns and self.columns:
1643 l.append(s%(sc+'columns', ','.join(self.columns)))
1644 if sort and self.sort[1] is not None:
1645 if self.sort[0] == '-':
1646 val = '-'+self.sort[1]
1647 else:
1648 val = self.sort[1]
1649 l.append(s%(sc+'sort', val))
1650 if group and self.group[1] is not None:
1651 if self.group[0] == '-':
1652 val = '-'+self.group[1]
1653 else:
1654 val = self.group[1]
1655 l.append(s%(sc+'group', val))
1656 if filter and self.filter:
1657 l.append(s%(sc+'filter', ','.join(self.filter)))
1658 if filterspec:
1659 for k,v in self.filterspec.items():
1660 if type(v) == type([]):
1661 l.append(s%(k, ','.join(v)))
1662 else:
1663 l.append(s%(k, v))
1664 if self.search_text:
1665 l.append(s%(sc+'search_text', self.search_text))
1666 l.append(s%(sc+'pagesize', self.pagesize))
1667 l.append(s%(sc+'startwith', self.startwith))
1668 return '\n'.join(l)
1670 def indexargs_url(self, url, args):
1671 ''' Embed the current index args in a URL
1672 '''
1673 sc = self.special_char
1674 l = ['%s=%s'%(k,v) for k,v in args.items()]
1676 # pull out the special values (prefixed by @ or :)
1677 specials = {}
1678 for key in args.keys():
1679 if key[0] in '@:':
1680 specials[key[1:]] = args[key]
1682 # ok, now handle the specials we received in the request
1683 if self.columns and not specials.has_key('columns'):
1684 l.append(sc+'columns=%s'%(','.join(self.columns)))
1685 if self.sort[1] is not None and not specials.has_key('sort'):
1686 if self.sort[0] == '-':
1687 val = '-'+self.sort[1]
1688 else:
1689 val = self.sort[1]
1690 l.append(sc+'sort=%s'%val)
1691 if self.group[1] is not None and not specials.has_key('group'):
1692 if self.group[0] == '-':
1693 val = '-'+self.group[1]
1694 else:
1695 val = self.group[1]
1696 l.append(sc+'group=%s'%val)
1697 if self.filter and not specials.has_key('filter'):
1698 l.append(sc+'filter=%s'%(','.join(self.filter)))
1699 if self.search_text and not specials.has_key('search_text'):
1700 l.append(sc+'search_text=%s'%self.search_text)
1701 if not specials.has_key('pagesize'):
1702 l.append(sc+'pagesize=%s'%self.pagesize)
1703 if not specials.has_key('startwith'):
1704 l.append(sc+'startwith=%s'%self.startwith)
1706 # finally, the remainder of the filter args in the request
1707 for k,v in self.filterspec.items():
1708 if not args.has_key(k):
1709 if type(v) == type([]):
1710 l.append('%s=%s'%(k, ','.join(v)))
1711 else:
1712 l.append('%s=%s'%(k, v))
1713 return '%s?%s'%(url, '&'.join(l))
1714 indexargs_href = indexargs_url
1716 def base_javascript(self):
1717 return '''
1718 <script type="text/javascript">
1719 submitted = false;
1720 function submit_once() {
1721 if (submitted) {
1722 alert("Your request is being processed.\\nPlease be patient.");
1723 return 0;
1724 }
1725 submitted = true;
1726 return 1;
1727 }
1729 function help_window(helpurl, width, height) {
1730 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1731 }
1732 </script>
1733 '''%self.base
1735 def batch(self):
1736 ''' Return a batch object for results from the "current search"
1737 '''
1738 filterspec = self.filterspec
1739 sort = self.sort
1740 group = self.group
1742 # get the list of ids we're batching over
1743 klass = self.client.db.getclass(self.classname)
1744 if self.search_text:
1745 matches = self.client.db.indexer.search(
1746 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1747 else:
1748 matches = None
1749 l = klass.filter(matches, filterspec, sort, group)
1751 # return the batch object, using IDs only
1752 return Batch(self.client, l, self.pagesize, self.startwith,
1753 classname=self.classname)
1755 # extend the standard ZTUtils Batch object to remove dependency on
1756 # Acquisition and add a couple of useful methods
1757 class Batch(ZTUtils.Batch):
1758 ''' Use me to turn a list of items, or item ids of a given class, into a
1759 series of batches.
1761 ========= ========================================================
1762 Parameter Usage
1763 ========= ========================================================
1764 sequence a list of HTMLItems or item ids
1765 classname if sequence is a list of ids, this is the class of item
1766 size how big to make the sequence.
1767 start where to start (0-indexed) in the sequence.
1768 end where to end (0-indexed) in the sequence.
1769 orphan if the next batch would contain less items than this
1770 value, then it is combined with this batch
1771 overlap the number of items shared between adjacent batches
1772 ========= ========================================================
1774 Attributes: Note that the "start" attribute, unlike the
1775 argument, is a 1-based index (I know, lame). "first" is the
1776 0-based index. "length" is the actual number of elements in
1777 the batch.
1779 "sequence_length" is the length of the original, unbatched, sequence.
1780 '''
1781 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1782 overlap=0, classname=None):
1783 self.client = client
1784 self.last_index = self.last_item = None
1785 self.current_item = None
1786 self.classname = classname
1787 self.sequence_length = len(sequence)
1788 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1789 overlap)
1791 # overwrite so we can late-instantiate the HTMLItem instance
1792 def __getitem__(self, index):
1793 if index < 0:
1794 if index + self.end < self.first: raise IndexError, index
1795 return self._sequence[index + self.end]
1797 if index >= self.length:
1798 raise IndexError, index
1800 # move the last_item along - but only if the fetched index changes
1801 # (for some reason, index 0 is fetched twice)
1802 if index != self.last_index:
1803 self.last_item = self.current_item
1804 self.last_index = index
1806 item = self._sequence[index + self.first]
1807 if self.classname:
1808 # map the item ids to instances
1809 if self.classname == 'user':
1810 item = HTMLUser(self.client, self.classname, item)
1811 else:
1812 item = HTMLItem(self.client, self.classname, item)
1813 self.current_item = item
1814 return item
1816 def propchanged(self, property):
1817 ''' Detect if the property marked as being the group property
1818 changed in the last iteration fetch
1819 '''
1820 if (self.last_item is None or
1821 self.last_item[property] != self.current_item[property]):
1822 return 1
1823 return 0
1825 # override these 'cos we don't have access to acquisition
1826 def previous(self):
1827 if self.start == 1:
1828 return None
1829 return Batch(self.client, self._sequence, self._size,
1830 self.first - self._size + self.overlap, 0, self.orphan,
1831 self.overlap)
1833 def next(self):
1834 try:
1835 self._sequence[self.end]
1836 except IndexError:
1837 return None
1838 return Batch(self.client, self._sequence, self._size,
1839 self.end - self.overlap, 0, self.orphan, self.overlap)
1841 class TemplatingUtils:
1842 ''' Utilities for templating
1843 '''
1844 def __init__(self, client):
1845 self.client = client
1846 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1847 return Batch(self.client, sequence, size, start, end, orphan,
1848 overlap)