81695009a719c6a80a4aa59eb6077e4a4d51882d
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')
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