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