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