bcc0a16157a5dcbbf2ddb29d142c120b7da7d0ef
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')
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