Code

bcc0a16157a5dcbbf2ddb29d142c120b7da7d0ef
[roundup.git] / roundup / cgi / client.py
1 # $Id: client.py,v 1.1 2002-08-30 08:28:44 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     }
300     def handle_action(self):
301         ''' Determine whether there should be an _action called.
303             The action is defined by the form variable :action which
304             identifies the method on this object to call. The four basic
305             actions are defined in the "actions" dictionary on this class:
306              "edit"      -> self.edititem_action
307              "new"       -> self.newitem_action
308              "login"     -> self.login_action
309              "logout"    -> self.logout_action
310              "register"  -> self.register_action
312         '''
313         if not self.form.has_key(':action'):
314             return None
315         try:
316             # get the action, validate it
317             action = self.form[':action'].value
318             if not self.actions.has_key(action):
319                 raise ValueError, 'No such action "%s"'%action
321             # call the mapped action
322             getattr(self, self.actions[action])()
323         except Redirect:
324             raise
325         except:
326             self.db.rollback()
327             s = StringIO.StringIO()
328             traceback.print_exc(None, s)
329             self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
331     def write(self, content):
332         if not self.headers_done:
333             self.header()
334         self.request.wfile.write(content)
336     def header(self, headers=None, response=200):
337         '''Put up the appropriate header.
338         '''
339         if headers is None:
340             headers = {'Content-Type':'text/html'}
341         if not headers.has_key('Content-Type'):
342             headers['Content-Type'] = 'text/html'
343         self.request.send_response(response)
344         for entry in headers.items():
345             self.request.send_header(*entry)
346         self.request.end_headers()
347         self.headers_done = 1
348         if self.debug:
349             self.headers_sent = headers
351     def set_cookie(self, user, password):
352         # TODO generate a much, much stronger session key ;)
353         self.session = binascii.b2a_base64(repr(time.time())).strip()
355         # clean up the base64
356         if self.session[-1] == '=':
357             if self.session[-2] == '=':
358                 self.session = self.session[:-2]
359             else:
360                 self.session = self.session[:-1]
362         # insert the session in the sessiondb
363         self.db.sessions.set(self.session, user=user, last_use=time.time())
365         # and commit immediately
366         self.db.sessions.commit()
368         # expire us in a long, long time
369         expire = Cookie._getdate(86400*365)
371         # generate the cookie path - make sure it has a trailing '/'
372         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
373             ''))
374         self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;'%(
375             self.session, expire, path)})
377     def make_user_anonymous(self):
378         ''' Make us anonymous
380             This method used to handle non-existence of the 'anonymous'
381             user, but that user is mandatory now.
382         '''
383         self.userid = self.db.user.lookup('anonymous')
384         self.user = 'anonymous'
386     def logout(self):
387         ''' Make us really anonymous - nuke the cookie too
388         '''
389         self.make_user_anonymous()
391         # construct the logout cookie
392         now = Cookie._getdate()
393         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
394             ''))
395         self.header({'Set-Cookie':
396             'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now,
397             path)})
398         self.login()
400     def opendb(self, user):
401         ''' Open the database.
402         '''
403         # open the db if the user has changed
404         if not hasattr(self, 'db') or user != self.db.journaltag:
405             self.db = self.instance.open(user)
407     #
408     # Actions
409     #
410     def login_action(self):
411         ''' Attempt to log a user in and set the cookie
412         '''
413         # we need the username at a minimum
414         if not self.form.has_key('__login_name'):
415             self.error_message.append(_('Username required'))
416             return
418         self.user = self.form['__login_name'].value
419         # re-open the database for real, using the user
420         self.opendb(self.user)
421         if self.form.has_key('__login_password'):
422             password = self.form['__login_password'].value
423         else:
424             password = ''
425         # make sure the user exists
426         try:
427             self.userid = self.db.user.lookup(self.user)
428         except KeyError:
429             name = self.user
430             self.make_user_anonymous()
431             self.error_message.append(_('No such user "%(name)s"')%locals())
432             return
434         # and that the password is correct
435         pw = self.db.user.get(self.userid, 'password')
436         if password != pw:
437             self.make_user_anonymous()
438             self.error_message.append(_('Incorrect password'))
439             return
441         # set the session cookie
442         self.set_cookie(self.user, password)
444     def logout_action(self):
445         ''' Make us really anonymous - nuke the cookie too
446         '''
447         # log us out
448         self.make_user_anonymous()
450         # construct the logout cookie
451         now = Cookie._getdate()
452         path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
453             ''))
454         self.header(headers={'Set-Cookie':
455             'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
456 #            'Location': self.db.config.DEFAULT_VIEW}, response=301)
458         # suboptimal, but will do for now
459         self.ok_message.append(_('You are logged out'))
460         #raise Redirect, None
462     def register_action(self):
463         '''Attempt to create a new user based on the contents of the form
464         and then set the cookie.
466         return 1 on successful login
467         '''
468         # make sure we're allowed to register
469         userid = self.db.user.lookup(self.user)
470         if not self.db.security.hasPermission('Web Registration', userid):
471             raise Unauthorised, _("You do not have permission to access"\
472                         " %(action)s.")%{'action': 'registration'}
474         # re-open the database as "admin"
475         if self.user != 'admin':
476             self.opendb('admin')
477             
478         # create the new user
479         cl = self.db.user
480         try:
481             props = parsePropsFromForm(self.db, cl, self.form)
482             props['roles'] = self.instance.NEW_WEB_USER_ROLES
483             uid = cl.create(**props)
484             self.db.commit()
485         except ValueError, message:
486             self.error_message.append(message)
488         # log the new user in
489         self.user = cl.get(uid, 'username')
490         # re-open the database for real, using the user
491         self.opendb(self.user)
492         password = cl.get(uid, 'password')
493         self.set_cookie(self.user, password)
495         # nice message
496         self.ok_message.append(_('You are now registered, welcome!'))
498     def edititem_action(self):
499         ''' Perform an edit of an item in the database.
501             Some special form elements:
503             :link=designator:property
504             :multilink=designator:property
505              The value specifies a node designator and the property on that
506              node to add _this_ node to as a link or multilink.
507             __note
508              Create a message and attach it to the current node's
509              "messages" property.
510             __file
511              Create a file and attach it to the current node's
512              "files" property. Attach the file to the message created from
513              the __note if it's supplied.
514         '''
515         cn = self.classname
516         cl = self.db.classes[cn]
518         # check permission
519         userid = self.db.user.lookup(self.user)
520         if not self.db.security.hasPermission('Edit', userid, cn):
521             self.error_message.append(
522                 _('You do not have permission to edit %s' %cn))
524         # perform the edit
525         props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
527         # make changes to the node
528         props = self._changenode(props)
530         # handle linked nodes 
531         self._post_editnode(self.nodeid)
533         # commit now that all the tricky stuff is done
534         self.db.commit()
536         # and some nice feedback for the user
537         if props:
538             message = _('%(changes)s edited ok')%{'changes':
539                 ', '.join(props.keys())}
540         elif self.form.has_key('__note') and self.form['__note'].value:
541             message = _('note added')
542         elif (self.form.has_key('__file') and self.form['__file'].filename):
543             message = _('file added')
544         else:
545             message = _('nothing changed')
547         # redirect to the item's edit page
548         raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, cn, self.nodeid,  
549             urllib.quote(message))
551     def newitem_action(self):
552         ''' Add a new item to the database.
554             This follows the same form as the edititem_action
555         '''
556         # check permission
557         cn = self.classname
558         userid = self.db.user.lookup(self.user)
559         if not self.db.security.hasPermission('Edit', userid, cn):
560             self.error_message.append(
561                 _('You do not have permission to create %s' %cn))
563         # XXX
564 #        cl = self.db.classes[cn]
565 #        if self.form.has_key(':multilink'):
566 #            link = self.form[':multilink'].value
567 #            designator, linkprop = link.split(':')
568 #            xtra = ' for <a href="%s">%s</a>' % (designator, designator)
569 #        else:
570 #            xtra = ''
572         try:
573             # do the create
574             nid = self._createnode()
576             # handle linked nodes 
577             self._post_editnode(nid)
579             # commit now that all the tricky stuff is done
580             self.db.commit()
582             # render the newly created item
583             self.nodeid = nid
585             # and some nice feedback for the user
586             message = _('%(classname)s created ok')%{'classname': cn}
587         except:
588             # oops
589             self.db.rollback()
590             s = StringIO.StringIO()
591             traceback.print_exc(None, s)
592             self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
594         # redirect to the new item's page
595         raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, cn, nid,  
596             urllib.quote(message))
598     def genericedit_action(self):
599         ''' Performs an edit of all of a class' items in one go.
601             The "rows" CGI var defines the CSV-formatted entries for the
602             class. New nodes are identified by the ID 'X' (or any other
603             non-existent ID) and removed lines are retired.
604         '''
605         userid = self.db.user.lookup(self.user)
606         if not self.db.security.hasPermission('Edit', userid):
607             raise Unauthorised, _("You do not have permission to access"\
608                         " %(action)s.")%{'action': self.classname}
609         w = self.write
610         cn = self.classname
611         cl = self.db.classes[cn]
612         idlessprops = cl.getprops(protected=0).keys()
613         props = ['id'] + idlessprops
615         # get the CSV module
616         try:
617             import csv
618         except ImportError:
619             self.error_message.append(_(
620                 'Sorry, you need the csv module to use this function.<br>\n'
621                 'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
622             return
624         # do the edit
625         rows = self.form['rows'].value.splitlines()
626         p = csv.parser()
627         found = {}
628         line = 0
629         for row in rows:
630             line += 1
631             values = p.parse(row)
632             # not a complete row, keep going
633             if not values: continue
635             # extract the nodeid
636             nodeid, values = values[0], values[1:]
637             found[nodeid] = 1
639             # confirm correct weight
640             if len(idlessprops) != len(values):
641                 w(_('Not enough values on line %(line)s'%{'line':line}))
642                 return
644             # extract the new values
645             d = {}
646             for name, value in zip(idlessprops, values):
647                 value = value.strip()
648                 # only add the property if it has a value
649                 if value:
650                     # if it's a multilink, split it
651                     if isinstance(cl.properties[name], hyperdb.Multilink):
652                         value = value.split(':')
653                     d[name] = value
655             # perform the edit
656             if cl.hasnode(nodeid):
657                 # edit existing
658                 cl.set(nodeid, **d)
659             else:
660                 # new node
661                 found[cl.create(**d)] = 1
663         # retire the removed entries
664         for nodeid in cl.list():
665             if not found.has_key(nodeid):
666                 cl.retire(nodeid)
668         message = _('items edited OK')
670         # redirect to the class' edit page
671         raise Redirect, '%s/%s?:ok_message=%s'%(self.base, cn, 
672             urllib.quote(message))
674     def _changenode(self, props):
675         ''' change the node based on the contents of the form
676         '''
677         cl = self.db.classes[self.classname]
679         # create the message
680         message, files = self._handle_message()
681         if message:
682             props['messages'] = cl.get(self.nodeid, 'messages') + [message]
683         if files:
684             props['files'] = cl.get(self.nodeid, 'files') + files
686         # make the changes
687         return cl.set(self.nodeid, **props)
689     def _createnode(self):
690         ''' create a node based on the contents of the form
691         '''
692         cl = self.db.classes[self.classname]
693         props = parsePropsFromForm(self.db, cl, self.form)
695         # check for messages and files
696         message, files = self._handle_message()
697         if message:
698             props['messages'] = [message]
699         if files:
700             props['files'] = files
701         # create the node and return it's id
702         return cl.create(**props)
704     def _handle_message(self):
705         ''' generate an edit message
706         '''
707         # handle file attachments 
708         files = []
709         if self.form.has_key('__file'):
710             file = self.form['__file']
711             if file.filename:
712                 filename = file.filename.split('\\')[-1]
713                 mime_type = mimetypes.guess_type(filename)[0]
714                 if not mime_type:
715                     mime_type = "application/octet-stream"
716                 # create the new file entry
717                 files.append(self.db.file.create(type=mime_type,
718                     name=filename, content=file.file.read()))
720         # we don't want to do a message if none of the following is true...
721         cn = self.classname
722         cl = self.db.classes[self.classname]
723         props = cl.getprops()
724         note = None
725         # in a nutshell, don't do anything if there's no note or there's no
726         # NOSY
727         if self.form.has_key('__note'):
728             note = self.form['__note'].value.strip()
729         if not note:
730             return None, files
731         if not props.has_key('messages'):
732             return None, files
733         if not isinstance(props['messages'], hyperdb.Multilink):
734             return None, files
735         if not props['messages'].classname == 'msg':
736             return None, files
737         if not (self.form.has_key('nosy') or note):
738             return None, files
740         # handle the note
741         if '\n' in note:
742             summary = re.split(r'\n\r?', note)[0]
743         else:
744             summary = note
745         m = ['%s\n'%note]
747         # handle the messageid
748         # TODO: handle inreplyto
749         messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
750             self.classname, self.instance.MAIL_DOMAIN)
752         # now create the message, attaching the files
753         content = '\n'.join(m)
754         message_id = self.db.msg.create(author=self.userid,
755             recipients=[], date=date.Date('.'), summary=summary,
756             content=content, files=files, messageid=messageid)
758         # update the messages property
759         return message_id, files
761     def _post_editnode(self, nid):
762         '''Do the linking part of the node creation.
764            If a form element has :link or :multilink appended to it, its
765            value specifies a node designator and the property on that node
766            to add _this_ node to as a link or multilink.
768            This is typically used on, eg. the file upload page to indicated
769            which issue to link the file to.
771            TODO: I suspect that this and newfile will go away now that
772            there's the ability to upload a file using the issue __file form
773            element!
774         '''
775         cn = self.classname
776         cl = self.db.classes[cn]
777         # link if necessary
778         keys = self.form.keys()
779         for key in keys:
780             if key == ':multilink':
781                 value = self.form[key].value
782                 if type(value) != type([]): value = [value]
783                 for value in value:
784                     designator, property = value.split(':')
785                     link, nodeid = hyperdb.splitDesignator(designator)
786                     link = self.db.classes[link]
787                     # take a dupe of the list so we're not changing the cache
788                     value = link.get(nodeid, property)[:]
789                     value.append(nid)
790                     link.set(nodeid, **{property: value})
791             elif key == ':link':
792                 value = self.form[key].value
793                 if type(value) != type([]): value = [value]
794                 for value in value:
795                     designator, property = value.split(':')
796                     link, nodeid = hyperdb.splitDesignator(designator)
797                     link = self.db.classes[link]
798                     link.set(nodeid, **{property: nid})
801     def remove_action(self,  dre=re.compile(r'([^\d]+)(\d+)')):
802         # XXX handle this !
803         target = self.index_arg(':target')[0]
804         m = dre.match(target)
805         if m:
806             classname = m.group(1)
807             nodeid = m.group(2)
808             cl = self.db.getclass(classname)
809             cl.retire(nodeid)
810             # now take care of the reference
811             parentref =  self.index_arg(':multilink')[0]
812             parent, prop = parentref.split(':')
813             m = dre.match(parent)
814             if m:
815                 self.classname = m.group(1)
816                 self.nodeid = m.group(2)
817                 cl = self.db.getclass(self.classname)
818                 value = cl.get(self.nodeid, prop)
819                 value.remove(nodeid)
820                 cl.set(self.nodeid, **{prop:value})
821                 func = getattr(self, 'show%s'%self.classname)
822                 return func()
823             else:
824                 raise NotFound, parent
825         else:
826             raise NotFound, target
829 def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
830     '''Pull properties for the given class out of the form.
831     '''
832     props = {}
833     keys = form.keys()
834     for key in keys:
835         if not cl.properties.has_key(key):
836             continue
837         proptype = cl.properties[key]
838         if isinstance(proptype, hyperdb.String):
839             value = form[key].value.strip()
840         elif isinstance(proptype, hyperdb.Password):
841             value = password.Password(form[key].value.strip())
842         elif isinstance(proptype, hyperdb.Date):
843             value = form[key].value.strip()
844             if value:
845                 value = date.Date(form[key].value.strip())
846             else:
847                 value = None
848         elif isinstance(proptype, hyperdb.Interval):
849             value = form[key].value.strip()
850             if value:
851                 value = date.Interval(form[key].value.strip())
852             else:
853                 value = None
854         elif isinstance(proptype, hyperdb.Link):
855             value = form[key].value.strip()
856             # see if it's the "no selection" choice
857             if value == '-1':
858                 value = None
859             else:
860                 # handle key values
861                 link = cl.properties[key].classname
862                 if not num_re.match(value):
863                     try:
864                         value = db.classes[link].lookup(value)
865                     except KeyError:
866                         raise ValueError, _('property "%(propname)s": '
867                             '%(value)s not a %(classname)s')%{'propname':key, 
868                             'value': value, 'classname': link}
869         elif isinstance(proptype, hyperdb.Multilink):
870             value = form[key]
871             if hasattr(value, 'value'):
872                 # Quite likely to be a FormItem instance
873                 value = value.value
874             if not isinstance(value, type([])):
875                 value = [i.strip() for i in value.split(',')]
876             else:
877                 value = [i.strip() for i in value]
878             link = cl.properties[key].classname
879             l = []
880             for entry in map(str, value):
881                 if entry == '': continue
882                 if not num_re.match(entry):
883                     try:
884                         entry = db.classes[link].lookup(entry)
885                     except KeyError:
886                         raise ValueError, _('property "%(propname)s": '
887                             '"%(value)s" not an entry of %(classname)s')%{
888                             'propname':key, 'value': entry, 'classname': link}
889                 l.append(entry)
890             l.sort()
891             value = l
892         elif isinstance(proptype, hyperdb.Boolean):
893             value = form[key].value.strip()
894             props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
895         elif isinstance(proptype, hyperdb.Number):
896             value = form[key].value.strip()
897             props[key] = value = int(value)
899         # get the old value
900         if nodeid:
901             try:
902                 existing = cl.get(nodeid, key)
903             except KeyError:
904                 # this might be a new property for which there is no existing
905                 # value
906                 if not cl.properties.has_key(key): raise
908             # if changed, set it
909             if value != existing:
910                 props[key] = value
911         else:
912             props[key] = value
913     return props