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