Code

81695009a719c6a80a4aa59eb6077e4a4d51882d
[roundup.git] / roundup / cgi / client.py
1 # $Id: client.py,v 1.2 2002-09-01 04:32:30 richard Exp $
3 __doc__ = """
4 WWW request handler (also used in the stand-alone server).
5 """
7 import os, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib
8 import binascii, Cookie, time, random
10 from roundup import roundupdb, date, hyperdb, password
11 from roundup.i18n import _
13 from roundup.cgi.templating import RoundupPageTemplate
14 from roundup.cgi import cgitb
15 from PageTemplates import PageTemplate
17 class Unauthorised(ValueError):
18     pass
20 class NotFound(ValueError):
21     pass
23 class Redirect(Exception):
24     pass
26 class SendFile(Exception):
27     ' Sent a file from the database '
29 class SendStaticFile(Exception):
30     ' Send a static file from the instance html directory '
32 def initialiseSecurity(security):
33     ''' Create some Permissions and Roles on the security object
35         This function is directly invoked by security.Security.__init__()
36         as a part of the Security object instantiation.
37     '''
38     security.addPermission(name="Web Registration",
39         description="User may register through the web")
40     p = security.addPermission(name="Web Access",
41         description="User may access the web interface")
42     security.addPermissionToRole('Admin', p)
44     # doing Role stuff through the web - make sure Admin can
45     p = security.addPermission(name="Web Roles",
46         description="User may manipulate user Roles through the web")
47     security.addPermissionToRole('Admin', p)
49 class Client:
50     '''
51     A note about login
52     ------------------
54     If the user has no login cookie, then they are anonymous. There
55     are two levels of anonymous use. If there is no 'anonymous' user, there
56     is no login at all and the database is opened in read-only mode. If the
57     'anonymous' user exists, the user is logged in using that user (though
58     there is no cookie). This allows them to modify the database, and all
59     modifications are attributed to the 'anonymous' user.
61     Once a user logs in, they are assigned a session. The Client instance
62     keeps the nodeid of the session as the "session" attribute.
63     '''
65     def __init__(self, instance, request, env, form=None):
66         hyperdb.traceMark()
67         self.instance = instance
68         self.request = request
69         self.env = env
70         self.path = env['PATH_INFO']
71         self.split_path = self.path.split('/')
72         self.instance_path_name = env['INSTANCE_NAME']
73         url = self.env['SCRIPT_NAME'] + '/' + self.instance_path_name
74         machine = self.env['SERVER_NAME']
75         port = self.env['SERVER_PORT']
76         if port != '80': machine = machine + ':' + port
77         self.base = urlparse.urlunparse(('http', env['HTTP_HOST'], url,
78             None, None, None))
80         if form is None:
81             self.form = cgi.FieldStorage(environ=env)
82         else:
83             self.form = form
84         self.headers_done = 0
85         try:
86             self.debug = int(env.get("ROUNDUP_DEBUG", 0))
87         except ValueError:
88             # someone gave us a non-int debug level, turn it off
89             self.debug = 0
91     def main(self):
92         ''' Wrap the request and handle unauthorised requests
93         '''
94         self.content_action = None
95         self.ok_message = []
96         self.error_message = []
97         try:
98             # make sure we're identified (even anonymously)
99             self.determine_user()
100             # figure out the context and desired content template
101             self.determine_context()
102             # possibly handle a form submit action (may change self.message
103             # and self.template_name)
104             self.handle_action()
105             # now render the page
106             self.write(self.template('page', ok_message=self.ok_message,
107                 error_message=self.error_message))
108         except Redirect, url:
109             # let's redirect - if the url isn't None, then we need to do
110             # the headers, otherwise the headers have been set before the
111             # exception was raised
112             if url:
113                 self.header({'Location': url}, response=302)
114         except SendFile, designator:
115             self.serve_file(designator)
116         except SendStaticFile, file:
117             self.serve_static_file(file)
118         except Unauthorised, message:
119             self.write(self.template('page.unauthorised',
120                 error_message=message))
121         except:
122             # everything else
123             self.write(cgitb.html())
125     def determine_user(self):
126         ''' Determine who the user is
127         '''
128         # determine the uid to use
129         self.opendb('admin')
131         # make sure we have the session Class
132         sessions = self.db.sessions
134         # age sessions, remove when they haven't been used for a week
135         # TODO: this shouldn't be done every access
136         week = 60*60*24*7
137         now = time.time()
138         for sessid in sessions.list():
139             interval = now - sessions.get(sessid, 'last_use')
140             if interval > week:
141                 sessions.destroy(sessid)
143         # look up the user session cookie
144         cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
145         user = 'anonymous'
147         if (cookie.has_key('roundup_user') and
148                 cookie['roundup_user'].value != 'deleted'):
150             # get the session key from the cookie
151             self.session = cookie['roundup_user'].value
152             # get the user from the session
153             try:
154                 # update the lifetime datestamp
155                 sessions.set(self.session, last_use=time.time())
156                 sessions.commit()
157                 user = sessions.get(self.session, 'user')
158             except KeyError:
159                 user = 'anonymous'
161         # sanity check on the user still being valid, getting the userid
162         # at the same time
163         try:
164             self.userid = self.db.user.lookup(user)
165         except (KeyError, TypeError):
166             user = 'anonymous'
168         # make sure the anonymous user is valid if we're using it
169         if user == 'anonymous':
170             self.make_user_anonymous()
171         else:
172             self.user = user
174     def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
175         ''' Determine the context of this page:
177              home              (default if no url is given)
178              classname
179              designator        (classname and nodeid)
181             The desired template to be rendered is also determined There
182             are two exceptional contexts:
184              _file            - serve up a static file
185              path len > 1     - serve up a FileClass content
186                                 (the additional path gives the browser a
187                                  nicer filename to save as)
189             The template used is specified by the :template CGI variable,
190             which defaults to:
191              only classname suplied:          "index"
192              full item designator supplied:   "item"
194             We set:
195              self.classname
196              self.nodeid
197              self.template_name
198         '''
199         # default the optional variables
200         self.classname = None
201         self.nodeid = None
203         # determine the classname and possibly nodeid
204         path = self.split_path
205         if not path or path[0] in ('', 'home', 'index'):
206             if self.form.has_key(':template'):
207                 self.template_type = self.form[':template'].value
208                 self.template_name = 'home' + '.' + self.template_type
209             else:
210                 self.template_type = ''
211                 self.template_name = 'home'
212             return
213         elif path[0] == '_file':
214             raise SendStaticFile, path[1]
215         else:
216             self.classname = path[0]
217             if len(path) > 1:
218                 # send the file identified by the designator in path[0]
219                 raise SendFile, path[0]
221         # see if we got a designator
222         m = dre.match(self.classname)
223         if m:
224             self.classname = m.group(1)
225             self.nodeid = m.group(2)
226             # with a designator, we default to item view
227             self.template_type = 'item'
228         else:
229             # with only a class, we default to index view
230             self.template_type = 'index'
232         # see if we have a template override
233         if self.form.has_key(':template'):
234             self.template_type = self.form[':template'].value
237         # see if we were passed in a message
238         if self.form.has_key(':ok_message'):
239             self.ok_message.append(self.form[':ok_message'].value)
240         if self.form.has_key(':error_message'):
241             self.error_message.append(self.form[':error_message'].value)
243         # we have the template name now
244         self.template_name = self.classname + '.' + self.template_type
246     def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
247         ''' Serve the file from the content property of the designated item.
248         '''
249         m = dre.match(str(designator))
250         if not m:
251             raise NotFound, str(designator)
252         classname, nodeid = m.group(1), m.group(2)
253         if classname != 'file':
254             raise NotFound, designator
256         # we just want to serve up the file named
257         file = self.db.file
258         self.header({'Content-Type': file.get(nodeid, 'type')})
259         self.write(file.get(nodeid, 'content'))
261     def serve_static_file(self, file):
262         # we just want to serve up the file named
263         mt = mimetypes.guess_type(str(file))[0]
264         self.header({'Content-Type': mt})
265         self.write(open('/tmp/test/html/%s'%file).read())
267     def template(self, name, **kwargs):
268         ''' Return a PageTemplate for the named page
269         '''
270         pt = RoundupPageTemplate(self)
271         # make errors nicer
272         pt.id = name
273         pt.write(open('/tmp/test/html/%s'%name).read())
274         # XXX handle PT rendering errors here nicely
275         try:
276             return pt.render(**kwargs)
277         except PageTemplate.PTRuntimeError, message:
278             return '<strong>%s</strong><ol>%s</ol>'%(message,
279                 cgi.escape('<li>'.join(pt._v_errors)))
280         except:
281             # everything else
282             return cgitb.html()
284     def content(self):
285         ''' Callback used by the page template to render the content of 
286             the page.
287         '''
288         # now render the page content using the template we determined in
289         # determine_context
290         return self.template(self.template_name)
292     # these are the actions that are available
293     actions = {
294         'edit':     'edititem_action',
295         'new':      'newitem_action',
296         'login':    'login_action',
297         'logout':   'logout_action',
298         'register': 'register_action',
299         'search':   'search_action',
300     }
301     def handle_action(self):
302         ''' Determine whether there should be an _action called.
304             The action is defined by the form variable :action which
305             identifies the method on this object to call. The four basic
306             actions are defined in the "actions" dictionary on this class:
307              "edit"      -> self.edititem_action
308              "new"       -> self.newitem_action
309              "login"     -> self.login_action
310              "logout"    -> self.logout_action
311              "register"  -> self.register_action
312              "search"    -> self.search_action
314         '''
315         if not self.form.has_key(':action'):
316             return None
317         try:
318             # get the action, validate it
319             action = self.form[':action'].value
320             if not self.actions.has_key(action):
321                 raise ValueError, 'No such action "%s"'%action
323             # call the mapped action
324             getattr(self, self.actions[action])()
325         except Redirect:
326             raise
327         except:
328             self.db.rollback()
329             s = StringIO.StringIO()
330             traceback.print_exc(None, s)
331             self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
333     def write(self, content):
334         if not self.headers_done:
335             self.header()
336         self.request.wfile.write(content)
338     def header(self, headers=None, response=200):
339         '''Put up the appropriate header.
340         '''
341         if headers is None:
342             headers = {'Content-Type':'text/html'}
343         if not headers.has_key('Content-Type'):
344             headers['Content-Type'] = 'text/html'
345         self.request.send_response(response)
346         for entry in headers.items():
347             self.request.send_header(*entry)
348         self.request.end_headers()
349         self.headers_done = 1
350         if self.debug:
351             self.headers_sent = headers
353     def set_cookie(self, user, password):
354         # TODO generate a much, much stronger session key ;)
355         self.session = binascii.b2a_base64(repr(time.time())).strip()
357         # clean up the base64
358         if self.session[-1] == '=':
359             if self.session[-2] == '=':
360                 self.session = self.session[:-2]
361             else:
362                 self.session = self.session[:-1]
364         # insert the session in the sessiondb
365         self.db.sessions.set(self.session, user=user, last_use=time.time())
367         # and commit immediately
368         self.db.sessions.commit()
370         # expire us in a long, long time
371         expire = Cookie._getdate(86400*365)
373         # generate the cookie path - make sure it has a trailing '/'
374         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
375             ''))
376         self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;'%(
377             self.session, expire, path)})
379     def make_user_anonymous(self):
380         ''' Make us anonymous
382             This method used to handle non-existence of the 'anonymous'
383             user, but that user is mandatory now.
384         '''
385         self.userid = self.db.user.lookup('anonymous')
386         self.user = 'anonymous'
388     def logout(self):
389         ''' Make us really anonymous - nuke the cookie too
390         '''
391         self.make_user_anonymous()
393         # construct the logout cookie
394         now = Cookie._getdate()
395         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
396             ''))
397         self.header({'Set-Cookie':
398             'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now,
399             path)})
400         self.login()
402     def opendb(self, user):
403         ''' Open the database.
404         '''
405         # open the db if the user has changed
406         if not hasattr(self, 'db') or user != self.db.journaltag:
407             self.db = self.instance.open(user)
409     #
410     # Actions
411     #
412     def login_action(self):
413         ''' Attempt to log a user in and set the cookie
414         '''
415         # we need the username at a minimum
416         if not self.form.has_key('__login_name'):
417             self.error_message.append(_('Username required'))
418             return
420         self.user = self.form['__login_name'].value
421         # re-open the database for real, using the user
422         self.opendb(self.user)
423         if self.form.has_key('__login_password'):
424             password = self.form['__login_password'].value
425         else:
426             password = ''
427         # make sure the user exists
428         try:
429             self.userid = self.db.user.lookup(self.user)
430         except KeyError:
431             name = self.user
432             self.make_user_anonymous()
433             self.error_message.append(_('No such user "%(name)s"')%locals())
434             return
436         # and that the password is correct
437         pw = self.db.user.get(self.userid, 'password')
438         if password != pw:
439             self.make_user_anonymous()
440             self.error_message.append(_('Incorrect password'))
441             return
443         # set the session cookie
444         self.set_cookie(self.user, password)
446     def logout_action(self):
447         ''' Make us really anonymous - nuke the cookie too
448         '''
449         # log us out
450         self.make_user_anonymous()
452         # construct the logout cookie
453         now = Cookie._getdate()
454         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
455             ''))
456         self.header(headers={'Set-Cookie':
457             'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
458 #            'Location': self.db.config.DEFAULT_VIEW}, response=301)
460         # suboptimal, but will do for now
461         self.ok_message.append(_('You are logged out'))
462         #raise Redirect, None
464     def register_action(self):
465         '''Attempt to create a new user based on the contents of the form
466         and then set the cookie.
468         return 1 on successful login
469         '''
470         # make sure we're allowed to register
471         userid = self.db.user.lookup(self.user)
472         if not self.db.security.hasPermission('Web Registration', userid):
473             raise Unauthorised, _("You do not have permission to access"\
474                         " %(action)s.")%{'action': 'registration'}
476         # re-open the database as "admin"
477         if self.user != 'admin':
478             self.opendb('admin')
479             
480         # create the new user
481         cl = self.db.user
482         try:
483             props = parsePropsFromForm(self.db, cl, self.form)
484             props['roles'] = self.instance.NEW_WEB_USER_ROLES
485             uid = cl.create(**props)
486             self.db.commit()
487         except ValueError, message:
488             self.error_message.append(message)
490         # log the new user in
491         self.user = cl.get(uid, 'username')
492         # re-open the database for real, using the user
493         self.opendb(self.user)
494         password = cl.get(uid, 'password')
495         self.set_cookie(self.user, password)
497         # nice message
498         self.ok_message.append(_('You are now registered, welcome!'))
500     def edititem_action(self):
501         ''' Perform an edit of an item in the database.
503             Some special form elements:
505             :link=designator:property
506             :multilink=designator:property
507              The value specifies a node designator and the property on that
508              node to add _this_ node to as a link or multilink.
509             __note
510              Create a message and attach it to the current node's
511              "messages" property.
512             __file
513              Create a file and attach it to the current node's
514              "files" property. Attach the file to the message created from
515              the __note if it's supplied.
516         '''
517         cl = self.db.classes[self.classname]
519         # check permission
520         userid = self.db.user.lookup(self.user)
521         if not self.db.security.hasPermission('Edit', userid, self.classname):
522             self.error_message.append(
523                 _('You do not have permission to edit %(classname)s' %
524                 self.__dict__))
525             return
527         # perform the edit
528         try:
529             props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
531             # make changes to the node
532             props = self._changenode(props)
534             # handle linked nodes 
535             self._post_editnode(self.nodeid)
537         except (ValueError, KeyError), message:
538             self.error_message.append(_('Error: ') + str(message))
539             return
541         # commit now that all the tricky stuff is done
542         self.db.commit()
544         # and some nice feedback for the user
545         if props:
546             message = _('%(changes)s edited ok')%{'changes':
547                 ', '.join(props.keys())}
548         elif self.form.has_key('__note') and self.form['__note'].value:
549             message = _('note added')
550         elif (self.form.has_key('__file') and self.form['__file'].filename):
551             message = _('file added')
552         else:
553             message = _('nothing changed')
555         # redirect to the item's edit page
556         raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
557             self.nodeid,  urllib.quote(message))
559     def newitem_action(self):
560         ''' Add a new item to the database.
562             This follows the same form as the edititem_action
563         '''
564         # check permission
565         userid = self.db.user.lookup(self.user)
566         if not self.db.security.hasPermission('Edit', userid, self.classname):
567             self.error_message.append(
568                 _('You do not have permission to create %s' %self.classname))
570         # XXX
571 #        cl = self.db.classes[cn]
572 #        if self.form.has_key(':multilink'):
573 #            link = self.form[':multilink'].value
574 #            designator, linkprop = link.split(':')
575 #            xtra = ' for <a href="%s">%s</a>' % (designator, designator)
576 #        else:
577 #            xtra = ''
579         try:
580             # do the create
581             nid = self._createnode()
583             # handle linked nodes 
584             self._post_editnode(nid)
586             # commit now that all the tricky stuff is done
587             self.db.commit()
589             # render the newly created item
590             self.nodeid = nid
592             # and some nice feedback for the user
593             message = _('%(classname)s created ok')%self.__dict__
594         except (ValueError, KeyError), message:
595             self.error_message.append(_('Error: ') + str(message))
596             return
597         except:
598             # oops
599             self.db.rollback()
600             s = StringIO.StringIO()
601             traceback.print_exc(None, s)
602             self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
603             return
605         # redirect to the new item's page
606         raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
607             nid,  urllib.quote(message))
609     def genericedit_action(self):
610         ''' Performs an edit of all of a class' items in one go.
612             The "rows" CGI var defines the CSV-formatted entries for the
613             class. New nodes are identified by the ID 'X' (or any other
614             non-existent ID) and removed lines are retired.
615         '''
616         userid = self.db.user.lookup(self.user)
617         if not self.db.security.hasPermission('Edit', userid, self.classname):
618             raise Unauthorised, _("You do not have permission to access"\
619                         " %(action)s.")%{'action': self.classname}
620         cl = self.db.classes[self.classname]
621         idlessprops = cl.getprops(protected=0).keys()
622         props = ['id'] + idlessprops
624         # get the CSV module
625         try:
626             import csv
627         except ImportError:
628             self.error_message.append(_(
629                 'Sorry, you need the csv module to use this function.<br>\n'
630                 'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
631             return
633         # do the edit
634         rows = self.form['rows'].value.splitlines()
635         p = csv.parser()
636         found = {}
637         line = 0
638         for row in rows:
639             line += 1
640             values = p.parse(row)
641             # not a complete row, keep going
642             if not values: continue
644             # extract the nodeid
645             nodeid, values = values[0], values[1:]
646             found[nodeid] = 1
648             # confirm correct weight
649             if len(idlessprops) != len(values):
650                 message=(_('Not enough values on line %(line)s'%{'line':line}))
651                 return
653             # extract the new values
654             d = {}
655             for name, value in zip(idlessprops, values):
656                 value = value.strip()
657                 # only add the property if it has a value
658                 if value:
659                     # if it's a multilink, split it
660                     if isinstance(cl.properties[name], hyperdb.Multilink):
661                         value = value.split(':')
662                     d[name] = value
664             # perform the edit
665             if cl.hasnode(nodeid):
666                 # edit existing
667                 cl.set(nodeid, **d)
668             else:
669                 # new node
670                 found[cl.create(**d)] = 1
672         # retire the removed entries
673         for nodeid in cl.list():
674             if not found.has_key(nodeid):
675                 cl.retire(nodeid)
677         message = _('items edited OK')
679         # redirect to the class' edit page
680         raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname, 
681             urllib.quote(message))
683     def _changenode(self, props):
684         ''' change the node based on the contents of the form
685         '''
686         cl = self.db.classes[self.classname]
688         # create the message
689         message, files = self._handle_message()
690         if message:
691             props['messages'] = cl.get(self.nodeid, 'messages') + [message]
692         if files:
693             props['files'] = cl.get(self.nodeid, 'files') + files
695         # make the changes
696         return cl.set(self.nodeid, **props)
698     def _createnode(self):
699         ''' create a node based on the contents of the form
700         '''
701         cl = self.db.classes[self.classname]
702         props = parsePropsFromForm(self.db, cl, self.form)
704         # check for messages and files
705         message, files = self._handle_message()
706         if message:
707             props['messages'] = [message]
708         if files:
709             props['files'] = files
710         # create the node and return it's id
711         return cl.create(**props)
713     def _handle_message(self):
714         ''' generate an edit message
715         '''
716         # handle file attachments 
717         files = []
718         if self.form.has_key('__file'):
719             file = self.form['__file']
720             if file.filename:
721                 filename = file.filename.split('\\')[-1]
722                 mime_type = mimetypes.guess_type(filename)[0]
723                 if not mime_type:
724                     mime_type = "application/octet-stream"
725                 # create the new file entry
726                 files.append(self.db.file.create(type=mime_type,
727                     name=filename, content=file.file.read()))
729         # we don't want to do a message if none of the following is true...
730         cn = self.classname
731         cl = self.db.classes[self.classname]
732         props = cl.getprops()
733         note = None
734         # in a nutshell, don't do anything if there's no note or there's no
735         # NOSY
736         if self.form.has_key('__note'):
737             note = self.form['__note'].value.strip()
738         if not note:
739             return None, files
740         if not props.has_key('messages'):
741             return None, files
742         if not isinstance(props['messages'], hyperdb.Multilink):
743             return None, files
744         if not props['messages'].classname == 'msg':
745             return None, files
746         if not (self.form.has_key('nosy') or note):
747             return None, files
749         # handle the note
750         if '\n' in note:
751             summary = re.split(r'\n\r?', note)[0]
752         else:
753             summary = note
754         m = ['%s\n'%note]
756         # handle the messageid
757         # TODO: handle inreplyto
758         messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
759             self.classname, self.instance.MAIL_DOMAIN)
761         # now create the message, attaching the files
762         content = '\n'.join(m)
763         message_id = self.db.msg.create(author=self.userid,
764             recipients=[], date=date.Date('.'), summary=summary,
765             content=content, files=files, messageid=messageid)
767         # update the messages property
768         return message_id, files
770     def _post_editnode(self, nid):
771         '''Do the linking part of the node creation.
773            If a form element has :link or :multilink appended to it, its
774            value specifies a node designator and the property on that node
775            to add _this_ node to as a link or multilink.
777            This is typically used on, eg. the file upload page to indicated
778            which issue to link the file to.
780            TODO: I suspect that this and newfile will go away now that
781            there's the ability to upload a file using the issue __file form
782            element!
783         '''
784         cn = self.classname
785         cl = self.db.classes[cn]
786         # link if necessary
787         keys = self.form.keys()
788         for key in keys:
789             if key == ':multilink':
790                 value = self.form[key].value
791                 if type(value) != type([]): value = [value]
792                 for value in value:
793                     designator, property = value.split(':')
794                     link, nodeid = hyperdb.splitDesignator(designator)
795                     link = self.db.classes[link]
796                     # take a dupe of the list so we're not changing the cache
797                     value = link.get(nodeid, property)[:]
798                     value.append(nid)
799                     link.set(nodeid, **{property: value})
800             elif key == ':link':
801                 value = self.form[key].value
802                 if type(value) != type([]): value = [value]
803                 for value in value:
804                     designator, property = value.split(':')
805                     link, nodeid = hyperdb.splitDesignator(designator)
806                     link = self.db.classes[link]
807                     link.set(nodeid, **{property: nid})
809     def search_action(self):
810         ''' Mangle some of the form variables.
812             Set the form ":filter" variable based on the values of the
813             filter variables - if they're set to anything other than
814             "dontcare" then add them to :filter.
815         '''
816         # add a faked :filter form variable for each filtering prop
817         props = self.db.classes[self.classname].getprops()
818         for key in self.form.keys():
819             if not props.has_key(key): continue
820             if not self.form[key].value: continue
821             self.form.value.append(cgi.MiniFieldStorage(':filter', key))
823     def remove_action(self,  dre=re.compile(r'([^\d]+)(\d+)')):
824         # XXX handle this !
825         target = self.index_arg(':target')[0]
826         m = dre.match(target)
827         if m:
828             classname = m.group(1)
829             nodeid = m.group(2)
830             cl = self.db.getclass(classname)
831             cl.retire(nodeid)
832             # now take care of the reference
833             parentref =  self.index_arg(':multilink')[0]
834             parent, prop = parentref.split(':')
835             m = dre.match(parent)
836             if m:
837                 self.classname = m.group(1)
838                 self.nodeid = m.group(2)
839                 cl = self.db.getclass(self.classname)
840                 value = cl.get(self.nodeid, prop)
841                 value.remove(nodeid)
842                 cl.set(self.nodeid, **{prop:value})
843                 func = getattr(self, 'show%s'%self.classname)
844                 return func()
845             else:
846                 raise NotFound, parent
847         else:
848             raise NotFound, target
851 def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
852     '''Pull properties for the given class out of the form.
853     '''
854     props = {}
855     keys = form.keys()
856     for key in keys:
857         if not cl.properties.has_key(key):
858             continue
859         proptype = cl.properties[key]
860         if isinstance(proptype, hyperdb.String):
861             value = form[key].value.strip()
862         elif isinstance(proptype, hyperdb.Password):
863             value = form[key].value.strip()
864             if not value:
865                 # ignore empty password values
866                 continue
867             value = password.Password(value)
868         elif isinstance(proptype, hyperdb.Date):
869             value = form[key].value.strip()
870             if value:
871                 value = date.Date(form[key].value.strip())
872             else:
873                 value = None
874         elif isinstance(proptype, hyperdb.Interval):
875             value = form[key].value.strip()
876             if value:
877                 value = date.Interval(form[key].value.strip())
878             else:
879                 value = None
880         elif isinstance(proptype, hyperdb.Link):
881             value = form[key].value.strip()
882             # see if it's the "no selection" choice
883             if value == '-1':
884                 value = None
885             else:
886                 # handle key values
887                 link = cl.properties[key].classname
888                 if not num_re.match(value):
889                     try:
890                         value = db.classes[link].lookup(value)
891                     except KeyError:
892                         raise ValueError, _('property "%(propname)s": '
893                             '%(value)s not a %(classname)s')%{'propname':key, 
894                             'value': value, 'classname': link}
895         elif isinstance(proptype, hyperdb.Multilink):
896             value = form[key]
897             if hasattr(value, 'value'):
898                 # Quite likely to be a FormItem instance
899                 value = value.value
900             if not isinstance(value, type([])):
901                 value = [i.strip() for i in value.split(',')]
902             else:
903                 value = [i.strip() for i in value]
904             link = cl.properties[key].classname
905             l = []
906             for entry in map(str, value):
907                 if entry == '': continue
908                 if not num_re.match(entry):
909                     try:
910                         entry = db.classes[link].lookup(entry)
911                     except KeyError:
912                         raise ValueError, _('property "%(propname)s": '
913                             '"%(value)s" not an entry of %(classname)s')%{
914                             'propname':key, 'value': entry, 'classname': link}
915                 l.append(entry)
916             l.sort()
917             value = l
918         elif isinstance(proptype, hyperdb.Boolean):
919             value = form[key].value.strip()
920             props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
921         elif isinstance(proptype, hyperdb.Number):
922             value = form[key].value.strip()
923             props[key] = value = int(value)
925         # get the old value
926         if nodeid:
927             try:
928                 existing = cl.get(nodeid, key)
929             except KeyError:
930                 # this might be a new property for which there is no existing
931                 # value
932                 if not cl.properties.has_key(key): raise
934             # if changed, set it
935             if value != existing:
936                 props[key] = value
937         else:
938             props[key] = value
939     return props