1 import sys, cgi, urllib, os, re, os.path, time, errno
3 from roundup import hyperdb, date
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 = filename + '.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 getItem(self, itemid, num_re=re.compile('\d+')):
345 ''' Get an item of this class by its item id.
346 '''
347 # make sure we're looking at an itemid
348 if not num_re.match(itemid):
349 itemid = self._klass.lookup(itemid)
351 if self.classname == 'user':
352 klass = HTMLUser
353 else:
354 klass = HTMLItem
356 return klass(self._client, self.classname, itemid)
358 def properties(self, sort=1):
359 ''' Return HTMLProperty for all of this class' properties.
360 '''
361 l = []
362 for name, prop in self._props.items():
363 for klass, htmlklass in propclasses:
364 if isinstance(prop, hyperdb.Multilink):
365 value = []
366 else:
367 value = None
368 if isinstance(prop, klass):
369 l.append(htmlklass(self._client, self._classname, '',
370 prop, name, value, self._anonymous))
371 if sort:
372 l.sort(lambda a,b:cmp(a._name, b._name))
373 return l
375 def list(self):
376 ''' List all items in this class.
377 '''
378 if self.classname == 'user':
379 klass = HTMLUser
380 else:
381 klass = HTMLItem
383 # get the list and sort it nicely
384 l = self._klass.list()
385 sortfunc = make_sort_function(self._db, self.classname)
386 l.sort(sortfunc)
388 l = [klass(self._client, self.classname, x) for x in l]
389 return l
391 def csv(self):
392 ''' Return the items of this class as a chunk of CSV text.
393 '''
394 # get the CSV module
395 try:
396 import csv
397 except ImportError:
398 return 'Sorry, you need the csv module to use this function.\n'\
399 'Get it from: http://www.object-craft.com.au/projects/csv/'
401 props = self.propnames()
402 p = csv.parser()
403 s = StringIO.StringIO()
404 s.write(p.join(props) + '\n')
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 s.write(p.join(l) + '\n')
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 'template=help&properties=%s%s\', \'%s\', \'%s\')">%s</a>'%(
471 self.classname, properties, property, width, height, label)
473 def submit(self, label="Submit New Entry"):
474 ''' Generate a submit button (and action hidden element)
475 '''
476 return ' <input type="hidden" name=":action" value="new">\n'\
477 ' <input type="submit" name="submit" value="%s">'%label
479 def history(self):
480 return 'New node - no history'
482 def renderWith(self, name, **kwargs):
483 ''' Render this class with the given template.
484 '''
485 # create a new request and override the specified args
486 req = HTMLRequest(self._client)
487 req.classname = self.classname
488 req.update(kwargs)
490 # new template, using the specified classname and request
491 pt = Templates(self._db.config.TEMPLATES).get(self.classname, name)
493 # use our fabricated request
494 return pt.render(self._client, self.classname, req)
496 class HTMLItem(HTMLPermissions):
497 ''' Accesses through an *item*
498 '''
499 def __init__(self, client, classname, nodeid, anonymous=0):
500 self._client = client
501 self._db = client.db
502 self._classname = classname
503 self._nodeid = nodeid
504 self._klass = self._db.getclass(classname)
505 self._props = self._klass.getprops()
507 # do we prefix the form items with the item's identification?
508 self._anonymous = anonymous
510 def __repr__(self):
511 return '<HTMLItem(0x%x) %s %s>'%(id(self), self._classname,
512 self._nodeid)
514 def __getitem__(self, item):
515 ''' return an HTMLProperty instance
516 '''
517 #print 'HTMLItem.getitem', (self, item)
518 if item == 'id':
519 return self._nodeid
521 # get the property
522 prop = self._props[item]
524 # get the value, handling missing values
525 value = None
526 if int(self._nodeid) > 0:
527 value = self._klass.get(self._nodeid, item, None)
528 if value is None:
529 if isinstance(self._props[item], hyperdb.Multilink):
530 value = []
532 # look up the correct HTMLProperty class
533 for klass, htmlklass in propclasses:
534 if isinstance(prop, klass):
535 return htmlklass(self._client, self._classname,
536 self._nodeid, prop, item, value, self._anonymous)
538 raise KeyError, item
540 def __getattr__(self, attr):
541 ''' convenience access to properties '''
542 try:
543 return self[attr]
544 except KeyError:
545 raise AttributeError, attr
547 def submit(self, label="Submit Changes"):
548 ''' Generate a submit button (and action hidden element)
549 '''
550 return ' <input type="hidden" name=":action" value="edit">\n'\
551 ' <input type="submit" name="submit" value="%s">'%label
553 def journal(self, direction='descending'):
554 ''' Return a list of HTMLJournalEntry instances.
555 '''
556 # XXX do this
557 return []
559 def history(self, direction='descending', dre=re.compile('\d+')):
560 l = ['<table class="history">'
561 '<tr><th colspan="4" class="header">',
562 _('History'),
563 '</th></tr><tr>',
564 _('<th>Date</th>'),
565 _('<th>User</th>'),
566 _('<th>Action</th>'),
567 _('<th>Args</th>'),
568 '</tr>']
569 current = {}
570 comments = {}
571 history = self._klass.history(self._nodeid)
572 history.sort()
573 timezone = self._db.getUserTimezone()
574 if direction == 'descending':
575 history.reverse()
576 for prop_n in self._props.keys():
577 prop = self[prop_n]
578 if isinstance(prop, HTMLProperty):
579 current[prop_n] = prop.plain()
580 # make link if hrefable
581 if (self._props.has_key(prop_n) and
582 isinstance(self._props[prop_n], hyperdb.Link)):
583 classname = self._props[prop_n].classname
584 try:
585 find_template(self._db.config.TEMPLATES,
586 classname, 'item')
587 except NoTemplate:
588 pass
589 else:
590 id = self._klass.get(self._nodeid, prop_n, None)
591 current[prop_n] = '<a href="%s%s">%s</a>'%(
592 classname, id, current[prop_n])
594 for id, evt_date, user, action, args in history:
595 date_s = str(evt_date.local(timezone)).replace("."," ")
596 arg_s = ''
597 if action == 'link' and type(args) == type(()):
598 if len(args) == 3:
599 linkcl, linkid, key = args
600 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
601 linkcl, linkid, key)
602 else:
603 arg_s = str(args)
605 elif action == 'unlink' and type(args) == type(()):
606 if len(args) == 3:
607 linkcl, linkid, key = args
608 arg_s += '<a href="%s%s">%s%s %s</a>'%(linkcl, linkid,
609 linkcl, linkid, key)
610 else:
611 arg_s = str(args)
613 elif type(args) == type({}):
614 cell = []
615 for k in args.keys():
616 # try to get the relevant property and treat it
617 # specially
618 try:
619 prop = self._props[k]
620 except KeyError:
621 prop = None
622 if prop is not None:
623 if args[k] and (isinstance(prop, hyperdb.Multilink) or
624 isinstance(prop, hyperdb.Link)):
625 # figure what the link class is
626 classname = prop.classname
627 try:
628 linkcl = self._db.getclass(classname)
629 except KeyError:
630 labelprop = None
631 comments[classname] = _('''The linked class
632 %(classname)s no longer exists''')%locals()
633 labelprop = linkcl.labelprop(1)
634 hrefable = os.path.exists(
635 os.path.join(self._db.config.TEMPLATES,
636 classname+'.item'))
638 if isinstance(prop, hyperdb.Multilink) and args[k]:
639 ml = []
640 for linkid in args[k]:
641 if isinstance(linkid, type(())):
642 sublabel = linkid[0] + ' '
643 linkids = linkid[1]
644 else:
645 sublabel = ''
646 linkids = [linkid]
647 subml = []
648 for linkid in linkids:
649 label = classname + linkid
650 # if we have a label property, try to use it
651 # TODO: test for node existence even when
652 # there's no labelprop!
653 try:
654 if labelprop is not None and \
655 labelprop != 'id':
656 label = linkcl.get(linkid, labelprop)
657 except IndexError:
658 comments['no_link'] = _('''<strike>The
659 linked node no longer
660 exists</strike>''')
661 subml.append('<strike>%s</strike>'%label)
662 else:
663 if hrefable:
664 subml.append('<a href="%s%s">%s</a>'%(
665 classname, linkid, label))
666 else:
667 subml.append(label)
668 ml.append(sublabel + ', '.join(subml))
669 cell.append('%s:\n %s'%(k, ', '.join(ml)))
670 elif isinstance(prop, hyperdb.Link) and args[k]:
671 label = classname + args[k]
672 # if we have a label property, try to use it
673 # TODO: test for node existence even when
674 # there's no labelprop!
675 if labelprop is not None and labelprop != 'id':
676 try:
677 label = linkcl.get(args[k], labelprop)
678 except IndexError:
679 comments['no_link'] = _('''<strike>The
680 linked node no longer
681 exists</strike>''')
682 cell.append(' <strike>%s</strike>,\n'%label)
683 # "flag" this is done .... euwww
684 label = None
685 if label is not None:
686 if hrefable:
687 old = '<a href="%s%s">%s</a>'%(classname, args[k], label)
688 else:
689 old = label;
690 cell.append('%s: %s' % (k,old))
691 if current.has_key(k):
692 cell[-1] += ' -> %s'%current[k]
693 current[k] = old
695 elif isinstance(prop, hyperdb.Date) and args[k]:
696 d = date.Date(args[k]).local(timezone)
697 cell.append('%s: %s'%(k, str(d)))
698 if current.has_key(k):
699 cell[-1] += ' -> %s' % current[k]
700 current[k] = str(d)
702 elif isinstance(prop, hyperdb.Interval) and args[k]:
703 d = date.Interval(args[k])
704 cell.append('%s: %s'%(k, str(d)))
705 if current.has_key(k):
706 cell[-1] += ' -> %s'%current[k]
707 current[k] = str(d)
709 elif isinstance(prop, hyperdb.String) and args[k]:
710 cell.append('%s: %s'%(k, cgi.escape(args[k])))
711 if current.has_key(k):
712 cell[-1] += ' -> %s'%current[k]
713 current[k] = cgi.escape(args[k])
715 elif not args[k]:
716 if current.has_key(k):
717 cell.append('%s: %s'%(k, current[k]))
718 current[k] = '(no value)'
719 else:
720 cell.append('%s: (no value)'%k)
722 else:
723 cell.append('%s: %s'%(k, str(args[k])))
724 if current.has_key(k):
725 cell[-1] += ' -> %s'%current[k]
726 current[k] = str(args[k])
727 else:
728 # property no longer exists
729 comments['no_exist'] = _('''<em>The indicated property
730 no longer exists</em>''')
731 cell.append('<em>%s: %s</em>\n'%(k, str(args[k])))
732 arg_s = '<br />'.join(cell)
733 else:
734 # unkown event!!
735 comments['unknown'] = _('''<strong><em>This event is not
736 handled by the history display!</em></strong>''')
737 arg_s = '<strong><em>' + str(args) + '</em></strong>'
738 date_s = date_s.replace(' ', ' ')
739 # if the user's an itemid, figure the username (older journals
740 # have the username)
741 if dre.match(user):
742 user = self._db.user.get(user, 'username')
743 l.append('<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>'%(
744 date_s, user, action, arg_s))
745 if comments:
746 l.append(_('<tr><td colspan=4><strong>Note:</strong></td></tr>'))
747 for entry in comments.values():
748 l.append('<tr><td colspan=4>%s</td></tr>'%entry)
749 l.append('</table>')
750 return '\n'.join(l)
752 def renderQueryForm(self):
753 ''' Render this item, which is a query, as a search form.
754 '''
755 # create a new request and override the specified args
756 req = HTMLRequest(self._client)
757 req.classname = self._klass.get(self._nodeid, 'klass')
758 name = self._klass.get(self._nodeid, 'name')
759 req.updateFromURL(self._klass.get(self._nodeid, 'url') +
760 '&:queryname=%s'%urllib.quote(name))
762 # new template, using the specified classname and request
763 pt = Templates(self._db.config.TEMPLATES).get(req.classname, 'search')
765 # use our fabricated request
766 return pt.render(self._client, req.classname, req)
768 class HTMLUser(HTMLItem):
769 ''' Accesses through the *user* (a special case of item)
770 '''
771 def __init__(self, client, classname, nodeid, anonymous=0):
772 HTMLItem.__init__(self, client, 'user', nodeid, anonymous)
773 self._default_classname = client.classname
775 # used for security checks
776 self._security = client.db.security
778 _marker = []
779 def hasPermission(self, permission, classname=_marker):
780 ''' Determine if the user has the Permission.
782 The class being tested defaults to the template's class, but may
783 be overidden for this test by suppling an alternate classname.
784 '''
785 if classname is self._marker:
786 classname = self._default_classname
787 return self._security.hasPermission(permission, self._nodeid, classname)
789 def is_edit_ok(self):
790 ''' Is the user allowed to Edit the current class?
791 Also check whether this is the current user's info.
792 '''
793 return self._db.security.hasPermission('Edit', self._client.userid,
794 self._classname) or self._nodeid == self._client.userid
796 def is_view_ok(self):
797 ''' Is the user allowed to View the current class?
798 Also check whether this is the current user's info.
799 '''
800 return self._db.security.hasPermission('Edit', self._client.userid,
801 self._classname) or self._nodeid == self._client.userid
803 class HTMLProperty:
804 ''' String, Number, Date, Interval HTMLProperty
806 Has useful attributes:
808 _name the name of the property
809 _value the value of the property if any
811 A wrapper object which may be stringified for the plain() behaviour.
812 '''
813 def __init__(self, client, classname, nodeid, prop, name, value,
814 anonymous=0):
815 self._client = client
816 self._db = client.db
817 self._classname = classname
818 self._nodeid = nodeid
819 self._prop = prop
820 self._value = value
821 self._anonymous = anonymous
822 self._name = name
823 if not anonymous:
824 self._formname = '%s%s@%s'%(classname, nodeid, name)
825 else:
826 self._formname = name
827 def __repr__(self):
828 return '<HTMLProperty(0x%x) %s %r %r>'%(id(self), self._formname,
829 self._prop, self._value)
830 def __str__(self):
831 return self.plain()
832 def __cmp__(self, other):
833 if isinstance(other, HTMLProperty):
834 return cmp(self._value, other._value)
835 return cmp(self._value, other)
837 class StringHTMLProperty(HTMLProperty):
838 hyper_re = re.compile(r'((?P<url>\w{3,6}://\S+)|'
839 r'(?P<email>[-+=%/\w\.]+@[\w\.\-]+)|'
840 r'(?P<item>(?P<class>[a-z_]+)(?P<id>\d+)))')
841 def _hyper_repl(self, match):
842 if match.group('url'):
843 s = match.group('url')
844 return '<a href="%s">%s</a>'%(s, s)
845 elif match.group('email'):
846 s = match.group('email')
847 return '<a href="mailto:%s">%s</a>'%(s, s)
848 else:
849 s = match.group('item')
850 s1 = match.group('class')
851 s2 = match.group('id')
852 try:
853 # make sure s1 is a valid tracker classname
854 self._db.getclass(s1)
855 return '<a href="%s">%s %s</a>'%(s, s1, s2)
856 except KeyError:
857 return '%s%s'%(s1, s2)
859 def plain(self, escape=0, hyperlink=0):
860 ''' Render a "plain" representation of the property
862 "escape" turns on/off HTML quoting
863 "hyperlink" turns on/off in-text hyperlinking of URLs, email
864 addresses and designators
865 '''
866 if self._value is None:
867 return ''
868 if escape:
869 s = cgi.escape(str(self._value))
870 else:
871 s = str(self._value)
872 if hyperlink:
873 if not escape:
874 s = cgi.escape(s)
875 s = self.hyper_re.sub(self._hyper_repl, s)
876 return s
878 def stext(self, escape=0):
879 ''' Render the value of the property as StructuredText.
881 This requires the StructureText module to be installed separately.
882 '''
883 s = self.plain(escape=escape)
884 if not StructuredText:
885 return s
886 return StructuredText(s,level=1,header=0)
888 def field(self, size = 30):
889 ''' Render a form edit field for the property
890 '''
891 if self._value is None:
892 value = ''
893 else:
894 value = cgi.escape(str(self._value))
895 value = '"'.join(value.split('"'))
896 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
898 def multiline(self, escape=0, rows=5, cols=40):
899 ''' Render a multiline form edit field for the property
900 '''
901 if self._value is None:
902 value = ''
903 else:
904 value = cgi.escape(str(self._value))
905 value = '"'.join(value.split('"'))
906 return '<textarea name="%s" rows="%s" cols="%s">%s</textarea>'%(
907 self._formname, rows, cols, value)
909 def email(self, escape=1):
910 ''' Render the value of the property as an obscured email address
911 '''
912 if self._value is None: value = ''
913 else: value = str(self._value)
914 if value.find('@') != -1:
915 name, domain = value.split('@')
916 domain = ' '.join(domain.split('.')[:-1])
917 name = name.replace('.', ' ')
918 value = '%s at %s ...'%(name, domain)
919 else:
920 value = value.replace('.', ' ')
921 if escape:
922 value = cgi.escape(value)
923 return value
925 class PasswordHTMLProperty(HTMLProperty):
926 def plain(self):
927 ''' Render a "plain" representation of the property
928 '''
929 if self._value is None:
930 return ''
931 return _('*encrypted*')
933 def field(self, size = 30):
934 ''' Render a form edit field for the property.
935 '''
936 return '<input type="password" name="%s" size="%s">'%(self._formname, size)
938 def confirm(self, size = 30):
939 ''' Render a second form edit field for the property, used for
940 confirmation that the user typed the password correctly. Generates
941 a field with name ":confirm:name".
942 '''
943 return '<input type="password" name=":confirm:%s" size="%s">'%(
944 self._formname, size)
946 class NumberHTMLProperty(HTMLProperty):
947 def plain(self):
948 ''' Render a "plain" representation of the property
949 '''
950 return str(self._value)
952 def field(self, size = 30):
953 ''' Render a form edit field for the property
954 '''
955 if self._value is None:
956 value = ''
957 else:
958 value = cgi.escape(str(self._value))
959 value = '"'.join(value.split('"'))
960 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
962 def __int__(self):
963 ''' Return an int of me
964 '''
965 return int(self._value)
967 def __float__(self):
968 ''' Return a float of me
969 '''
970 return float(self._value)
973 class BooleanHTMLProperty(HTMLProperty):
974 def plain(self):
975 ''' Render a "plain" representation of the property
976 '''
977 if self._value is None:
978 return ''
979 return self._value and "Yes" or "No"
981 def field(self):
982 ''' Render a form edit field for the property
983 '''
984 checked = self._value and "checked" or ""
985 s = '<input type="radio" name="%s" value="yes" %s>Yes'%(self._formname,
986 checked)
987 if checked:
988 checked = ""
989 else:
990 checked = "checked"
991 s += '<input type="radio" name="%s" value="no" %s>No'%(self._formname,
992 checked)
993 return s
995 class DateHTMLProperty(HTMLProperty):
996 def plain(self):
997 ''' Render a "plain" representation of the property
998 '''
999 if self._value is None:
1000 return ''
1001 return str(self._value.local(self._db.getUserTimezone()))
1003 def now(self):
1004 ''' Return the current time.
1006 This is useful for defaulting a new value. Returns a
1007 DateHTMLProperty.
1008 '''
1009 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1010 self._formname, date.Date('.'))
1012 def field(self, size = 30):
1013 ''' Render a form edit field for the property
1014 '''
1015 if self._value is None:
1016 value = ''
1017 else:
1018 value = cgi.escape(str(self._value.local(self._db.getUserTimezone())))
1019 value = '"'.join(value.split('"'))
1020 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1022 def reldate(self, pretty=1):
1023 ''' Render the interval between the date and now.
1025 If the "pretty" flag is true, then make the display pretty.
1026 '''
1027 if not self._value:
1028 return ''
1030 # figure the interval
1031 interval = date.Date('.') - self._value
1032 if pretty:
1033 return interval.pretty()
1034 return str(interval)
1036 _marker = []
1037 def pretty(self, format=_marker):
1038 ''' Render the date in a pretty format (eg. month names, spaces).
1040 The format string is a standard python strftime format string.
1041 Note that if the day is zero, and appears at the start of the
1042 string, then it'll be stripped from the output. This is handy
1043 for the situatin when a date only specifies a month and a year.
1044 '''
1045 if format is not self._marker:
1046 return self._value.pretty(format)
1047 else:
1048 return self._value.pretty()
1050 def local(self, offset):
1051 ''' Return the date/time as a local (timezone offset) date/time.
1052 '''
1053 return DateHTMLProperty(self._client, self._nodeid, self._prop,
1054 self._formname, self._value.local(offset))
1056 class IntervalHTMLProperty(HTMLProperty):
1057 def plain(self):
1058 ''' Render a "plain" representation of the property
1059 '''
1060 if self._value is None:
1061 return ''
1062 return str(self._value)
1064 def pretty(self):
1065 ''' Render the interval in a pretty format (eg. "yesterday")
1066 '''
1067 return self._value.pretty()
1069 def field(self, size = 30):
1070 ''' Render a form edit field for the property
1071 '''
1072 if self._value is None:
1073 value = ''
1074 else:
1075 value = cgi.escape(str(self._value))
1076 value = '"'.join(value.split('"'))
1077 return '<input name="%s" value="%s" size="%s">'%(self._formname, value, size)
1079 class LinkHTMLProperty(HTMLProperty):
1080 ''' Link HTMLProperty
1081 Include the above as well as being able to access the class
1082 information. Stringifying the object itself results in the value
1083 from the item being displayed. Accessing attributes of this object
1084 result in the appropriate entry from the class being queried for the
1085 property accessed (so item/assignedto/name would look up the user
1086 entry identified by the assignedto property on item, and then the
1087 name property of that user)
1088 '''
1089 def __init__(self, *args, **kw):
1090 HTMLProperty.__init__(self, *args, **kw)
1091 # if we're representing a form value, then the -1 from the form really
1092 # should be a None
1093 if str(self._value) == '-1':
1094 self._value = None
1096 def __getattr__(self, attr):
1097 ''' return a new HTMLItem '''
1098 #print 'Link.getattr', (self, attr, self._value)
1099 if not self._value:
1100 raise AttributeError, "Can't access missing value"
1101 if self._prop.classname == 'user':
1102 klass = HTMLUser
1103 else:
1104 klass = HTMLItem
1105 i = klass(self._client, self._prop.classname, self._value)
1106 return getattr(i, attr)
1108 def plain(self, escape=0):
1109 ''' Render a "plain" representation of the property
1110 '''
1111 if self._value is None:
1112 return ''
1113 linkcl = self._db.classes[self._prop.classname]
1114 k = linkcl.labelprop(1)
1115 value = str(linkcl.get(self._value, k))
1116 if escape:
1117 value = cgi.escape(value)
1118 return value
1120 def field(self, showid=0, size=None):
1121 ''' Render a form edit field for the property
1122 '''
1123 linkcl = self._db.getclass(self._prop.classname)
1124 if linkcl.getprops().has_key('order'):
1125 sort_on = 'order'
1126 else:
1127 sort_on = linkcl.labelprop()
1128 options = linkcl.filter(None, {}, ('+', sort_on), (None, None))
1129 # TODO: make this a field display, not a menu one!
1130 l = ['<select name="%s">'%self._formname]
1131 k = linkcl.labelprop(1)
1132 if self._value is None:
1133 s = 'selected '
1134 else:
1135 s = ''
1136 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1138 # make sure we list the current value if it's retired
1139 if self._value and self._value not in options:
1140 options.insert(0, self._value)
1142 for optionid in options:
1143 # get the option value, and if it's None use an empty string
1144 option = linkcl.get(optionid, k) or ''
1146 # figure if this option is selected
1147 s = ''
1148 if optionid == self._value:
1149 s = 'selected '
1151 # figure the label
1152 if showid:
1153 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1154 else:
1155 lab = option
1157 # truncate if it's too long
1158 if size is not None and len(lab) > size:
1159 lab = lab[:size-3] + '...'
1161 # and generate
1162 lab = cgi.escape(lab)
1163 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1164 l.append('</select>')
1165 return '\n'.join(l)
1167 def menu(self, size=None, height=None, showid=0, additional=[],
1168 **conditions):
1169 ''' Render a form select list for this property
1170 '''
1171 value = self._value
1173 # sort function
1174 sortfunc = make_sort_function(self._db, self._prop.classname)
1176 linkcl = self._db.getclass(self._prop.classname)
1177 l = ['<select name="%s">'%self._formname]
1178 k = linkcl.labelprop(1)
1179 s = ''
1180 if value is None:
1181 s = 'selected '
1182 l.append(_('<option %svalue="-1">- no selection -</option>')%s)
1183 if linkcl.getprops().has_key('order'):
1184 sort_on = ('+', 'order')
1185 else:
1186 sort_on = ('+', linkcl.labelprop())
1187 options = linkcl.filter(None, conditions, sort_on, (None, None))
1189 # make sure we list the current value if it's retired
1190 if self._value and self._value not in options:
1191 options.insert(0, self._value)
1193 for optionid in options:
1194 # get the option value, and if it's None use an empty string
1195 option = linkcl.get(optionid, k) or ''
1197 # figure if this option is selected
1198 s = ''
1199 if value in [optionid, option]:
1200 s = 'selected '
1202 # figure the label
1203 if showid:
1204 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1205 else:
1206 lab = option
1208 # truncate if it's too long
1209 if size is not None and len(lab) > size:
1210 lab = lab[:size-3] + '...'
1211 if additional:
1212 m = []
1213 for propname in additional:
1214 m.append(linkcl.get(optionid, propname))
1215 lab = lab + ' (%s)'%', '.join(map(str, m))
1217 # and generate
1218 lab = cgi.escape(lab)
1219 l.append('<option %svalue="%s">%s</option>'%(s, optionid, lab))
1220 l.append('</select>')
1221 return '\n'.join(l)
1222 # def checklist(self, ...)
1224 class MultilinkHTMLProperty(HTMLProperty):
1225 ''' Multilink HTMLProperty
1227 Also be iterable, returning a wrapper object like the Link case for
1228 each entry in the multilink.
1229 '''
1230 def __len__(self):
1231 ''' length of the multilink '''
1232 return len(self._value)
1234 def __getattr__(self, attr):
1235 ''' no extended attribute accesses make sense here '''
1236 raise AttributeError, attr
1238 def __getitem__(self, num):
1239 ''' iterate and return a new HTMLItem
1240 '''
1241 #print 'Multi.getitem', (self, num)
1242 value = self._value[num]
1243 if self._prop.classname == 'user':
1244 klass = HTMLUser
1245 else:
1246 klass = HTMLItem
1247 return klass(self._client, self._prop.classname, value)
1249 def __contains__(self, value):
1250 ''' Support the "in" operator. We have to make sure the passed-in
1251 value is a string first, not a *HTMLProperty.
1252 '''
1253 return str(value) in self._value
1255 def reverse(self):
1256 ''' return the list in reverse order
1257 '''
1258 l = self._value[:]
1259 l.reverse()
1260 if self._prop.classname == 'user':
1261 klass = HTMLUser
1262 else:
1263 klass = HTMLItem
1264 return [klass(self._client, self._prop.classname, value) for value in l]
1266 def plain(self, escape=0):
1267 ''' Render a "plain" representation of the property
1268 '''
1269 linkcl = self._db.classes[self._prop.classname]
1270 k = linkcl.labelprop(1)
1271 labels = []
1272 for v in self._value:
1273 labels.append(linkcl.get(v, k))
1274 value = ', '.join(labels)
1275 if escape:
1276 value = cgi.escape(value)
1277 return value
1279 def field(self, size=30, showid=0):
1280 ''' Render a form edit field for the property
1281 '''
1282 sortfunc = make_sort_function(self._db, self._prop.classname)
1283 linkcl = self._db.getclass(self._prop.classname)
1284 value = self._value[:]
1285 if value:
1286 value.sort(sortfunc)
1287 # map the id to the label property
1288 if not linkcl.getkey():
1289 showid=1
1290 if not showid:
1291 k = linkcl.labelprop(1)
1292 value = [linkcl.get(v, k) for v in value]
1293 value = cgi.escape(','.join(value))
1294 return '<input name="%s" size="%s" value="%s">'%(self._formname, size, value)
1296 def menu(self, size=None, height=None, showid=0, additional=[],
1297 **conditions):
1298 ''' Render a form select list for this property
1299 '''
1300 value = self._value
1302 # sort function
1303 sortfunc = make_sort_function(self._db, self._prop.classname)
1305 linkcl = self._db.getclass(self._prop.classname)
1306 if linkcl.getprops().has_key('order'):
1307 sort_on = ('+', 'order')
1308 else:
1309 sort_on = ('+', linkcl.labelprop())
1310 options = linkcl.filter(None, conditions, sort_on, (None,None))
1311 height = height or min(len(options), 7)
1312 l = ['<select multiple name="%s" size="%s">'%(self._formname, height)]
1313 k = linkcl.labelprop(1)
1315 # make sure we list the current values if they're retired
1316 for val in value:
1317 if val not in options:
1318 options.insert(0, val)
1320 for optionid in options:
1321 # get the option value, and if it's None use an empty string
1322 option = linkcl.get(optionid, k) or ''
1324 # figure if this option is selected
1325 s = ''
1326 if optionid in value or option in value:
1327 s = 'selected '
1329 # figure the label
1330 if showid:
1331 lab = '%s%s: %s'%(self._prop.classname, optionid, option)
1332 else:
1333 lab = option
1334 # truncate if it's too long
1335 if size is not None and len(lab) > size:
1336 lab = lab[:size-3] + '...'
1337 if additional:
1338 m = []
1339 for propname in additional:
1340 m.append(linkcl.get(optionid, propname))
1341 lab = lab + ' (%s)'%', '.join(m)
1343 # and generate
1344 lab = cgi.escape(lab)
1345 l.append('<option %svalue="%s">%s</option>'%(s, optionid,
1346 lab))
1347 l.append('</select>')
1348 return '\n'.join(l)
1350 # set the propclasses for HTMLItem
1351 propclasses = (
1352 (hyperdb.String, StringHTMLProperty),
1353 (hyperdb.Number, NumberHTMLProperty),
1354 (hyperdb.Boolean, BooleanHTMLProperty),
1355 (hyperdb.Date, DateHTMLProperty),
1356 (hyperdb.Interval, IntervalHTMLProperty),
1357 (hyperdb.Password, PasswordHTMLProperty),
1358 (hyperdb.Link, LinkHTMLProperty),
1359 (hyperdb.Multilink, MultilinkHTMLProperty),
1360 )
1362 def make_sort_function(db, classname):
1363 '''Make a sort function for a given class
1364 '''
1365 linkcl = db.getclass(classname)
1366 if linkcl.getprops().has_key('order'):
1367 sort_on = 'order'
1368 else:
1369 sort_on = linkcl.labelprop()
1370 def sortfunc(a, b, linkcl=linkcl, sort_on=sort_on):
1371 return cmp(linkcl.get(a, sort_on), linkcl.get(b, sort_on))
1372 return sortfunc
1374 def handleListCGIValue(value):
1375 ''' Value is either a single item or a list of items. Each item has a
1376 .value that we're actually interested in.
1377 '''
1378 if isinstance(value, type([])):
1379 return [value.value for value in value]
1380 else:
1381 value = value.value.strip()
1382 if not value:
1383 return []
1384 return value.split(',')
1386 class ShowDict:
1387 ''' A convenience access to the :columns index parameters
1388 '''
1389 def __init__(self, columns):
1390 self.columns = {}
1391 for col in columns:
1392 self.columns[col] = 1
1393 def __getitem__(self, name):
1394 return self.columns.has_key(name)
1396 class HTMLRequest:
1397 ''' The *request*, holding the CGI form and environment.
1399 "form" the CGI form as a cgi.FieldStorage
1400 "env" the CGI environment variables
1401 "base" the base URL for this instance
1402 "user" a HTMLUser instance for this user
1403 "classname" the current classname (possibly None)
1404 "template" the current template (suffix, also possibly None)
1406 Index args:
1407 "columns" dictionary of the columns to display in an index page
1408 "show" a convenience access to columns - request/show/colname will
1409 be true if the columns should be displayed, false otherwise
1410 "sort" index sort column (direction, column name)
1411 "group" index grouping property (direction, column name)
1412 "filter" properties to filter the index on
1413 "filterspec" values to filter the index on
1414 "search_text" text to perform a full-text search on for an index
1416 '''
1417 def __init__(self, client):
1418 self.client = client
1420 # easier access vars
1421 self.form = client.form
1422 self.env = client.env
1423 self.base = client.base
1424 self.user = HTMLUser(client, 'user', client.userid)
1426 # store the current class name and action
1427 self.classname = client.classname
1428 self.template = client.template
1430 # the special char to use for special vars
1431 self.special_char = '@'
1433 self._post_init()
1435 def _post_init(self):
1436 ''' Set attributes based on self.form
1437 '''
1438 # extract the index display information from the form
1439 self.columns = []
1440 for name in ':columns @columns'.split():
1441 if self.form.has_key(name):
1442 self.special_char = name[0]
1443 self.columns = handleListCGIValue(self.form[name])
1444 break
1445 self.show = ShowDict(self.columns)
1447 # sorting
1448 self.sort = (None, None)
1449 for name in ':sort @sort'.split():
1450 if self.form.has_key(name):
1451 self.special_char = name[0]
1452 sort = self.form[name].value
1453 if sort.startswith('-'):
1454 self.sort = ('-', sort[1:])
1455 else:
1456 self.sort = ('+', sort)
1457 if self.form.has_key(self.special_char+'sortdir'):
1458 self.sort = ('-', self.sort[1])
1460 # grouping
1461 self.group = (None, None)
1462 for name in ':group @group'.split():
1463 if self.form.has_key(name):
1464 self.special_char = name[0]
1465 group = self.form[name].value
1466 if group.startswith('-'):
1467 self.group = ('-', group[1:])
1468 else:
1469 self.group = ('+', group)
1470 if self.form.has_key(self.special_char+'groupdir'):
1471 self.group = ('-', self.group[1])
1473 # filtering
1474 self.filter = []
1475 for name in ':filter @filter'.split():
1476 if self.form.has_key(name):
1477 self.special_char = name[0]
1478 self.filter = handleListCGIValue(self.form[name])
1480 self.filterspec = {}
1481 db = self.client.db
1482 if self.classname is not None:
1483 props = db.getclass(self.classname).getprops()
1484 for name in self.filter:
1485 if not self.form.has_key(name):
1486 continue
1487 prop = props[name]
1488 fv = self.form[name]
1489 if (isinstance(prop, hyperdb.Link) or
1490 isinstance(prop, hyperdb.Multilink)):
1491 self.filterspec[name] = lookupIds(db, prop,
1492 handleListCGIValue(fv))
1493 else:
1494 if isinstance(fv, type([])):
1495 self.filterspec[name] = [v.value for v in fv]
1496 else:
1497 self.filterspec[name] = fv.value
1499 # full-text search argument
1500 self.search_text = None
1501 for name in ':search_text @search_text'.split():
1502 if self.form.has_key(name):
1503 self.special_char = name[0]
1504 self.search_text = self.form[name].value
1506 # pagination - size and start index
1507 # figure batch args
1508 self.pagesize = 50
1509 for name in ':pagesize @pagesize'.split():
1510 if self.form.has_key(name):
1511 self.special_char = name[0]
1512 self.pagesize = int(self.form[name].value)
1514 self.startwith = 0
1515 for name in ':startwith @startwith'.split():
1516 if self.form.has_key(name):
1517 self.special_char = name[0]
1518 self.startwith = int(self.form[name].value)
1520 def updateFromURL(self, url):
1521 ''' Parse the URL for query args, and update my attributes using the
1522 values.
1523 '''
1524 env = {'QUERY_STRING': url}
1525 self.form = cgi.FieldStorage(environ=env)
1527 self._post_init()
1529 def update(self, kwargs):
1530 ''' Update my attributes using the keyword args
1531 '''
1532 self.__dict__.update(kwargs)
1533 if kwargs.has_key('columns'):
1534 self.show = ShowDict(self.columns)
1536 def description(self):
1537 ''' Return a description of the request - handle for the page title.
1538 '''
1539 s = [self.client.db.config.TRACKER_NAME]
1540 if self.classname:
1541 if self.client.nodeid:
1542 s.append('- %s%s'%(self.classname, self.client.nodeid))
1543 else:
1544 if self.template == 'item':
1545 s.append('- new %s'%self.classname)
1546 elif self.template == 'index':
1547 s.append('- %s index'%self.classname)
1548 else:
1549 s.append('- %s %s'%(self.classname, self.template))
1550 else:
1551 s.append('- home')
1552 return ' '.join(s)
1554 def __str__(self):
1555 d = {}
1556 d.update(self.__dict__)
1557 f = ''
1558 for k in self.form.keys():
1559 f += '\n %r=%r'%(k,handleListCGIValue(self.form[k]))
1560 d['form'] = f
1561 e = ''
1562 for k,v in self.env.items():
1563 e += '\n %r=%r'%(k, v)
1564 d['env'] = e
1565 return '''
1566 form: %(form)s
1567 base: %(base)r
1568 classname: %(classname)r
1569 template: %(template)r
1570 columns: %(columns)r
1571 sort: %(sort)r
1572 group: %(group)r
1573 filter: %(filter)r
1574 search_text: %(search_text)r
1575 pagesize: %(pagesize)r
1576 startwith: %(startwith)r
1577 env: %(env)s
1578 '''%d
1580 def indexargs_form(self, columns=1, sort=1, group=1, filter=1,
1581 filterspec=1):
1582 ''' return the current index args as form elements '''
1583 l = []
1584 sc = self.special_char
1585 s = '<input type="hidden" name="%s" value="%s">'
1586 if columns and self.columns:
1587 l.append(s%(sc+'columns', ','.join(self.columns)))
1588 if sort and self.sort[1] is not None:
1589 if self.sort[0] == '-':
1590 val = '-'+self.sort[1]
1591 else:
1592 val = self.sort[1]
1593 l.append(s%(sc+'sort', val))
1594 if group and self.group[1] is not None:
1595 if self.group[0] == '-':
1596 val = '-'+self.group[1]
1597 else:
1598 val = self.group[1]
1599 l.append(s%(sc+'group', val))
1600 if filter and self.filter:
1601 l.append(s%(sc+'filter', ','.join(self.filter)))
1602 if filterspec:
1603 for k,v in self.filterspec.items():
1604 if type(v) == type([]):
1605 l.append(s%(k, ','.join(v)))
1606 else:
1607 l.append(s%(k, v))
1608 if self.search_text:
1609 l.append(s%(sc+'search_text', self.search_text))
1610 l.append(s%(sc+'pagesize', self.pagesize))
1611 l.append(s%(sc+'startwith', self.startwith))
1612 return '\n'.join(l)
1614 def indexargs_url(self, url, args):
1615 ''' Embed the current index args in a URL
1616 '''
1617 sc = self.special_char
1618 l = ['%s=%s'%(k,v) for k,v in args.items()]
1620 # pull out the special values (prefixed by @ or :)
1621 specials = {}
1622 for key in args.keys():
1623 if key[0] in '@:':
1624 specials[key[1:]] = args[key]
1626 # ok, now handle the specials we received in the request
1627 if self.columns and not specials.has_key('columns'):
1628 l.append(sc+'columns=%s'%(','.join(self.columns)))
1629 if self.sort[1] is not None and not specials.has_key('sort'):
1630 if self.sort[0] == '-':
1631 val = '-'+self.sort[1]
1632 else:
1633 val = self.sort[1]
1634 l.append(sc+'sort=%s'%val)
1635 if self.group[1] is not None and not specials.has_key('group'):
1636 if self.group[0] == '-':
1637 val = '-'+self.group[1]
1638 else:
1639 val = self.group[1]
1640 l.append(sc+'group=%s'%val)
1641 if self.filter and not specials.has_key('filter'):
1642 l.append(sc+'filter=%s'%(','.join(self.filter)))
1643 if self.search_text and not specials.has_key('search_text'):
1644 l.append(sc+'search_text=%s'%self.search_text)
1645 if not specials.has_key('pagesize'):
1646 l.append(sc+'pagesize=%s'%self.pagesize)
1647 if not specials.has_key('startwith'):
1648 l.append(sc+'startwith=%s'%self.startwith)
1650 # finally, the remainder of the filter args in the request
1651 for k,v in self.filterspec.items():
1652 if not args.has_key(k):
1653 if type(v) == type([]):
1654 l.append('%s=%s'%(k, ','.join(v)))
1655 else:
1656 l.append('%s=%s'%(k, v))
1657 return '%s?%s'%(url, '&'.join(l))
1658 indexargs_href = indexargs_url
1660 def base_javascript(self):
1661 return '''
1662 <script language="javascript">
1663 submitted = false;
1664 function submit_once() {
1665 if (submitted) {
1666 alert("Your request is being processed.\\nPlease be patient.");
1667 return 0;
1668 }
1669 submitted = true;
1670 return 1;
1671 }
1673 function help_window(helpurl, width, height) {
1674 HelpWin = window.open('%s' + helpurl, 'RoundupHelpWindow', 'scrollbars=yes,resizable=yes,toolbar=no,height='+height+',width='+width);
1675 }
1676 </script>
1677 '''%self.base
1679 def batch(self):
1680 ''' Return a batch object for results from the "current search"
1681 '''
1682 filterspec = self.filterspec
1683 sort = self.sort
1684 group = self.group
1686 # get the list of ids we're batching over
1687 klass = self.client.db.getclass(self.classname)
1688 if self.search_text:
1689 matches = self.client.db.indexer.search(
1690 re.findall(r'\b\w{2,25}\b', self.search_text), klass)
1691 else:
1692 matches = None
1693 l = klass.filter(matches, filterspec, sort, group)
1695 # return the batch object, using IDs only
1696 return Batch(self.client, l, self.pagesize, self.startwith,
1697 classname=self.classname)
1699 # extend the standard ZTUtils Batch object to remove dependency on
1700 # Acquisition and add a couple of useful methods
1701 class Batch(ZTUtils.Batch):
1702 ''' Use me to turn a list of items, or item ids of a given class, into a
1703 series of batches.
1705 ========= ========================================================
1706 Parameter Usage
1707 ========= ========================================================
1708 sequence a list of HTMLItems or item ids
1709 classname if sequence is a list of ids, this is the class of item
1710 size how big to make the sequence.
1711 start where to start (0-indexed) in the sequence.
1712 end where to end (0-indexed) in the sequence.
1713 orphan if the next batch would contain less items than this
1714 value, then it is combined with this batch
1715 overlap the number of items shared between adjacent batches
1716 ========= ========================================================
1718 Attributes: Note that the "start" attribute, unlike the
1719 argument, is a 1-based index (I know, lame). "first" is the
1720 0-based index. "length" is the actual number of elements in
1721 the batch.
1723 "sequence_length" is the length of the original, unbatched, sequence.
1724 '''
1725 def __init__(self, client, sequence, size, start, end=0, orphan=0,
1726 overlap=0, classname=None):
1727 self.client = client
1728 self.last_index = self.last_item = None
1729 self.current_item = None
1730 self.classname = classname
1731 self.sequence_length = len(sequence)
1732 ZTUtils.Batch.__init__(self, sequence, size, start, end, orphan,
1733 overlap)
1735 # overwrite so we can late-instantiate the HTMLItem instance
1736 def __getitem__(self, index):
1737 if index < 0:
1738 if index + self.end < self.first: raise IndexError, index
1739 return self._sequence[index + self.end]
1741 if index >= self.length:
1742 raise IndexError, index
1744 # move the last_item along - but only if the fetched index changes
1745 # (for some reason, index 0 is fetched twice)
1746 if index != self.last_index:
1747 self.last_item = self.current_item
1748 self.last_index = index
1750 item = self._sequence[index + self.first]
1751 if self.classname:
1752 # map the item ids to instances
1753 if self.classname == 'user':
1754 item = HTMLUser(self.client, self.classname, item)
1755 else:
1756 item = HTMLItem(self.client, self.classname, item)
1757 self.current_item = item
1758 return item
1760 def propchanged(self, property):
1761 ''' Detect if the property marked as being the group property
1762 changed in the last iteration fetch
1763 '''
1764 if (self.last_item is None or
1765 self.last_item[property] != self.current_item[property]):
1766 return 1
1767 return 0
1769 # override these 'cos we don't have access to acquisition
1770 def previous(self):
1771 if self.start == 1:
1772 return None
1773 return Batch(self.client, self._sequence, self._size,
1774 self.first - self._size + self.overlap, 0, self.orphan,
1775 self.overlap)
1777 def next(self):
1778 try:
1779 self._sequence[self.end]
1780 except IndexError:
1781 return None
1782 return Batch(self.client, self._sequence, self._size,
1783 self.end - self.overlap, 0, self.orphan, self.overlap)
1785 class TemplatingUtils:
1786 ''' Utilities for templating
1787 '''
1788 def __init__(self, client):
1789 self.client = client
1790 def Batch(self, sequence, size, start, end=0, orphan=0, overlap=0):
1791 return Batch(self.client, sequence, size, start, end, orphan,
1792 overlap)