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