6924ad73d5e69bbe08b4463da3094b8564970915
1 # $Id: client.py,v 1.4 2002-09-01 22:09:20 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.
64 Client attributes:
65 "url" is the current url path
66 "path" is the PATH_INFO inside the instance
67 "base" is the base URL for the instance
68 '''
70 def __init__(self, instance, request, env, form=None):
71 hyperdb.traceMark()
72 self.instance = instance
73 self.request = request
74 self.env = env
76 self.path = env['PATH_INFO']
77 self.split_path = self.path.split('/')
78 self.instance_path_name = env['INSTANCE_NAME']
80 # this is the base URL for this instance
81 url = self.env['SCRIPT_NAME'] + '/' + self.instance_path_name
82 self.base = urlparse.urlunparse(('http', env['HTTP_HOST'], url,
83 None, None, None))
85 # request.path is the full request path
86 x, x, path, x, x, x = urlparse.urlparse(request.path)
87 self.url = urlparse.urlunparse(('http', env['HTTP_HOST'], path,
88 None, None, None))
90 if form is None:
91 self.form = cgi.FieldStorage(environ=env)
92 else:
93 self.form = form
94 self.headers_done = 0
95 try:
96 self.debug = int(env.get("ROUNDUP_DEBUG", 0))
97 except ValueError:
98 # someone gave us a non-int debug level, turn it off
99 self.debug = 0
101 def main(self):
102 ''' Wrap the request and handle unauthorised requests
103 '''
104 self.content_action = None
105 self.ok_message = []
106 self.error_message = []
107 try:
108 # make sure we're identified (even anonymously)
109 self.determine_user()
110 # figure out the context and desired content template
111 self.determine_context()
112 # possibly handle a form submit action (may change self.message
113 # and self.template_name)
114 self.handle_action()
115 # now render the page
116 self.write(self.template('page', ok_message=self.ok_message,
117 error_message=self.error_message))
118 except Redirect, url:
119 # let's redirect - if the url isn't None, then we need to do
120 # the headers, otherwise the headers have been set before the
121 # exception was raised
122 if url:
123 self.header({'Location': url}, response=302)
124 except SendFile, designator:
125 self.serve_file(designator)
126 except SendStaticFile, file:
127 self.serve_static_file(file)
128 except Unauthorised, message:
129 self.write(self.template('page.unauthorised',
130 error_message=message))
131 except:
132 # everything else
133 self.write(cgitb.html())
135 def determine_user(self):
136 ''' Determine who the user is
137 '''
138 # determine the uid to use
139 self.opendb('admin')
141 # make sure we have the session Class
142 sessions = self.db.sessions
144 # age sessions, remove when they haven't been used for a week
145 # TODO: this shouldn't be done every access
146 week = 60*60*24*7
147 now = time.time()
148 for sessid in sessions.list():
149 interval = now - sessions.get(sessid, 'last_use')
150 if interval > week:
151 sessions.destroy(sessid)
153 # look up the user session cookie
154 cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
155 user = 'anonymous'
157 if (cookie.has_key('roundup_user') and
158 cookie['roundup_user'].value != 'deleted'):
160 # get the session key from the cookie
161 self.session = cookie['roundup_user'].value
162 # get the user from the session
163 try:
164 # update the lifetime datestamp
165 sessions.set(self.session, last_use=time.time())
166 sessions.commit()
167 user = sessions.get(self.session, 'user')
168 except KeyError:
169 user = 'anonymous'
171 # sanity check on the user still being valid, getting the userid
172 # at the same time
173 try:
174 self.userid = self.db.user.lookup(user)
175 except (KeyError, TypeError):
176 user = 'anonymous'
178 # make sure the anonymous user is valid if we're using it
179 if user == 'anonymous':
180 self.make_user_anonymous()
181 else:
182 self.user = user
184 # reopen the database as the correct user
185 self.opendb(self.user)
187 def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
188 ''' Determine the context of this page:
190 home (default if no url is given)
191 classname
192 designator (classname and nodeid)
194 The desired template to be rendered is also determined There
195 are two exceptional contexts:
197 _file - serve up a static file
198 path len > 1 - serve up a FileClass content
199 (the additional path gives the browser a
200 nicer filename to save as)
202 The template used is specified by the :template CGI variable,
203 which defaults to:
204 only classname suplied: "index"
205 full item designator supplied: "item"
207 We set:
208 self.classname
209 self.nodeid
210 self.template_name
211 '''
212 # default the optional variables
213 self.classname = None
214 self.nodeid = None
216 # determine the classname and possibly nodeid
217 path = self.split_path
218 if not path or path[0] in ('', 'home', 'index'):
219 if self.form.has_key(':template'):
220 self.template_type = self.form[':template'].value
221 self.template_name = 'home' + '.' + self.template_type
222 else:
223 self.template_type = ''
224 self.template_name = 'home'
225 return
226 elif path[0] == '_file':
227 raise SendStaticFile, path[1]
228 else:
229 self.classname = path[0]
230 if len(path) > 1:
231 # send the file identified by the designator in path[0]
232 raise SendFile, path[0]
234 # see if we got a designator
235 m = dre.match(self.classname)
236 if m:
237 self.classname = m.group(1)
238 self.nodeid = m.group(2)
239 # with a designator, we default to item view
240 self.template_type = 'item'
241 else:
242 # with only a class, we default to index view
243 self.template_type = 'index'
245 # see if we have a template override
246 if self.form.has_key(':template'):
247 self.template_type = self.form[':template'].value
250 # see if we were passed in a message
251 if self.form.has_key(':ok_message'):
252 self.ok_message.append(self.form[':ok_message'].value)
253 if self.form.has_key(':error_message'):
254 self.error_message.append(self.form[':error_message'].value)
256 # we have the template name now
257 self.template_name = self.classname + '.' + self.template_type
259 def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
260 ''' Serve the file from the content property of the designated item.
261 '''
262 m = dre.match(str(designator))
263 if not m:
264 raise NotFound, str(designator)
265 classname, nodeid = m.group(1), m.group(2)
266 if classname != 'file':
267 raise NotFound, designator
269 # we just want to serve up the file named
270 file = self.db.file
271 self.header({'Content-Type': file.get(nodeid, 'type')})
272 self.write(file.get(nodeid, 'content'))
274 def serve_static_file(self, file):
275 # we just want to serve up the file named
276 mt = mimetypes.guess_type(str(file))[0]
277 self.header({'Content-Type': mt})
278 self.write(open('/tmp/test/html/%s'%file).read())
280 def template(self, name, **kwargs):
281 ''' Return a PageTemplate for the named page
282 '''
283 pt = RoundupPageTemplate(self)
284 # make errors nicer
285 pt.id = name
286 pt.write(open('/tmp/test/html/%s'%name).read())
287 # XXX handle PT rendering errors here nicely
288 try:
289 return pt.render(**kwargs)
290 except PageTemplate.PTRuntimeError, message:
291 return '<strong>%s</strong><ol>%s</ol>'%(message,
292 cgi.escape('<li>'.join(pt._v_errors)))
293 except:
294 # everything else
295 return cgitb.html()
297 def content(self):
298 ''' Callback used by the page template to render the content of
299 the page.
300 '''
301 # now render the page content using the template we determined in
302 # determine_context
303 return self.template(self.template_name)
305 # these are the actions that are available
306 actions = {
307 'edit': 'editItemAction',
308 'new': 'newItemAction',
309 'login': 'login_action',
310 'logout': 'logout_action',
311 'register': 'register_action',
312 'search': 'searchAction',
313 }
314 def handle_action(self):
315 ''' Determine whether there should be an _action called.
317 The action is defined by the form variable :action which
318 identifies the method on this object to call. The four basic
319 actions are defined in the "actions" dictionary on this class:
320 "edit" -> self.editItemAction
321 "new" -> self.newItemAction
322 "login" -> self.login_action
323 "logout" -> self.logout_action
324 "register" -> self.register_action
325 "search" -> self.searchAction
327 '''
328 if not self.form.has_key(':action'):
329 return None
330 try:
331 # get the action, validate it
332 action = self.form[':action'].value
333 if not self.actions.has_key(action):
334 raise ValueError, 'No such action "%s"'%action
336 # call the mapped action
337 getattr(self, self.actions[action])()
338 except Redirect:
339 raise
340 except:
341 self.db.rollback()
342 s = StringIO.StringIO()
343 traceback.print_exc(None, s)
344 self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
346 def write(self, content):
347 if not self.headers_done:
348 self.header()
349 self.request.wfile.write(content)
351 def header(self, headers=None, response=200):
352 '''Put up the appropriate header.
353 '''
354 if headers is None:
355 headers = {'Content-Type':'text/html'}
356 if not headers.has_key('Content-Type'):
357 headers['Content-Type'] = 'text/html'
358 self.request.send_response(response)
359 for entry in headers.items():
360 self.request.send_header(*entry)
361 self.request.end_headers()
362 self.headers_done = 1
363 if self.debug:
364 self.headers_sent = headers
366 def set_cookie(self, user, password):
367 # TODO generate a much, much stronger session key ;)
368 self.session = binascii.b2a_base64(repr(time.time())).strip()
370 # clean up the base64
371 if self.session[-1] == '=':
372 if self.session[-2] == '=':
373 self.session = self.session[:-2]
374 else:
375 self.session = self.session[:-1]
377 # insert the session in the sessiondb
378 self.db.sessions.set(self.session, user=user, last_use=time.time())
380 # and commit immediately
381 self.db.sessions.commit()
383 # expire us in a long, long time
384 expire = Cookie._getdate(86400*365)
386 # generate the cookie path - make sure it has a trailing '/'
387 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
388 ''))
389 self.header({'Set-Cookie': 'roundup_user=%s; expires=%s; Path=%s;'%(
390 self.session, expire, path)})
392 def make_user_anonymous(self):
393 ''' Make us anonymous
395 This method used to handle non-existence of the 'anonymous'
396 user, but that user is mandatory now.
397 '''
398 self.userid = self.db.user.lookup('anonymous')
399 self.user = 'anonymous'
401 def logout(self):
402 ''' Make us really anonymous - nuke the cookie too
403 '''
404 self.make_user_anonymous()
406 # construct the logout cookie
407 now = Cookie._getdate()
408 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
409 ''))
410 self.header({'Set-Cookie':
411 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now,
412 path)})
413 self.login()
415 def opendb(self, user):
416 ''' Open the database.
417 '''
418 # open the db if the user has changed
419 if not hasattr(self, 'db') or user != self.db.journaltag:
420 self.db = self.instance.open(user)
422 #
423 # Actions
424 #
425 def login_action(self):
426 ''' Attempt to log a user in and set the cookie
427 '''
428 # we need the username at a minimum
429 if not self.form.has_key('__login_name'):
430 self.error_message.append(_('Username required'))
431 return
433 self.user = self.form['__login_name'].value
434 # re-open the database for real, using the user
435 self.opendb(self.user)
436 if self.form.has_key('__login_password'):
437 password = self.form['__login_password'].value
438 else:
439 password = ''
440 # make sure the user exists
441 try:
442 self.userid = self.db.user.lookup(self.user)
443 except KeyError:
444 name = self.user
445 self.make_user_anonymous()
446 self.error_message.append(_('No such user "%(name)s"')%locals())
447 return
449 # and that the password is correct
450 pw = self.db.user.get(self.userid, 'password')
451 if password != pw:
452 self.make_user_anonymous()
453 self.error_message.append(_('Incorrect password'))
454 return
456 # set the session cookie
457 self.set_cookie(self.user, password)
459 def logout_action(self):
460 ''' Make us really anonymous - nuke the cookie too
461 '''
462 # log us out
463 self.make_user_anonymous()
465 # construct the logout cookie
466 now = Cookie._getdate()
467 path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
468 ''))
469 self.header(headers={'Set-Cookie':
470 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
472 # Let the user know what's going on
473 self.ok_message.append(_('You are logged out'))
475 def register_action(self):
476 '''Attempt to create a new user based on the contents of the form
477 and then set the cookie.
479 return 1 on successful login
480 '''
481 # make sure we're allowed to register
482 userid = self.db.user.lookup(self.user)
483 if not self.db.security.hasPermission('Web Registration', userid):
484 raise Unauthorised, _("You do not have permission to access"\
485 " %(action)s.")%{'action': 'registration'}
487 # re-open the database as "admin"
488 if self.user != 'admin':
489 self.opendb('admin')
491 # create the new user
492 cl = self.db.user
493 try:
494 props = parsePropsFromForm(self.db, cl, self.form)
495 props['roles'] = self.instance.NEW_WEB_USER_ROLES
496 uid = cl.create(**props)
497 self.db.commit()
498 except ValueError, message:
499 self.error_message.append(message)
501 # log the new user in
502 self.user = cl.get(uid, 'username')
503 # re-open the database for real, using the user
504 self.opendb(self.user)
505 password = cl.get(uid, 'password')
506 self.set_cookie(self.user, password)
508 # nice message
509 self.ok_message.append(_('You are now registered, welcome!'))
511 def editItemAction(self):
512 ''' Perform an edit of an item in the database.
514 Some special form elements:
516 :link=designator:property
517 :multilink=designator:property
518 The value specifies a node designator and the property on that
519 node to add _this_ node to as a link or multilink.
520 __note
521 Create a message and attach it to the current node's
522 "messages" property.
523 __file
524 Create a file and attach it to the current node's
525 "files" property. Attach the file to the message created from
526 the __note if it's supplied.
527 '''
528 cl = self.db.classes[self.classname]
530 # parse the props from the form
531 try:
532 props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
533 except (ValueError, KeyError), message:
534 self.error_message.append(_('Error: ') + str(message))
535 return
537 # check permission
538 if not self.editItemPermission(props):
539 self.error_message.append(
540 _('You do not have permission to edit %(classname)s'%
541 self.__dict__))
542 return
544 # perform the edit
545 try:
546 # make changes to the node
547 props = self._changenode(props)
548 # handle linked nodes
549 self._post_editnode(self.nodeid)
550 except (ValueError, KeyError), message:
551 self.error_message.append(_('Error: ') + str(message))
552 return
554 # commit now that all the tricky stuff is done
555 self.db.commit()
557 # and some nice feedback for the user
558 if props:
559 message = _('%(changes)s edited ok')%{'changes':
560 ', '.join(props.keys())}
561 elif self.form.has_key('__note') and self.form['__note'].value:
562 message = _('note added')
563 elif (self.form.has_key('__file') and self.form['__file'].filename):
564 message = _('file added')
565 else:
566 message = _('nothing changed')
568 # redirect to the item's edit page
569 raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
570 self.nodeid, urllib.quote(message))
572 def editItemPermission(self, props):
573 ''' Determine whether the user has permission to edit this item.
575 Base behaviour is to check the user can edit this class. If we're
576 editing the "user" class, users are allowed to edit their own
577 details. Unless it's the "roles" property, which requires the
578 special Permission "Web Roles".
579 '''
580 # if this is a user node and the user is editing their own node, then
581 # we're OK
582 has = self.db.security.hasPermission
583 if self.classname == 'user':
584 # reject if someone's trying to edit "roles" and doesn't have the
585 # right permission.
586 if props.has_key('roles') and not has('Web Roles', self.userid,
587 'user'):
588 return 0
589 # if the item being edited is the current user, we're ok
590 if self.nodeid == self.userid:
591 return 1
592 if not self.db.security.hasPermission('Edit', self.userid,
593 self.classname):
594 return 0
595 return 1
597 def newItemAction(self):
598 ''' Add a new item to the database.
600 This follows the same form as the editItemAction
601 '''
602 cl = self.db.classes[self.classname]
604 # parse the props from the form
605 try:
606 props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
607 except (ValueError, KeyError), message:
608 self.error_message.append(_('Error: ') + str(message))
609 return
611 if not self.newItemPermission(props):
612 self.error_message.append(
613 _('You do not have permission to create %s' %self.classname))
615 # XXX
616 # cl = self.db.classes[cn]
617 # if self.form.has_key(':multilink'):
618 # link = self.form[':multilink'].value
619 # designator, linkprop = link.split(':')
620 # xtra = ' for <a href="%s">%s</a>' % (designator, designator)
621 # else:
622 # xtra = ''
624 try:
625 # do the create
626 nid = self._createnode(props)
628 # handle linked nodes
629 self._post_editnode(nid)
631 # commit now that all the tricky stuff is done
632 self.db.commit()
634 # render the newly created item
635 self.nodeid = nid
637 # and some nice feedback for the user
638 message = _('%(classname)s created ok')%self.__dict__
639 except (ValueError, KeyError), message:
640 self.error_message.append(_('Error: ') + str(message))
641 return
642 except:
643 # oops
644 self.db.rollback()
645 s = StringIO.StringIO()
646 traceback.print_exc(None, s)
647 self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
648 return
650 # redirect to the new item's page
651 raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
652 nid, urllib.quote(message))
654 def newItemPermission(self, props):
655 ''' Determine whether the user has permission to create (edit) this
656 item.
658 Base behaviour is to check the user can edit this class. No
659 additional property checks are made. Additionally, new user items
660 may be created if the user has the "Web Registration" Permission.
661 '''
662 has = self.db.security.hasPermission
663 if self.classname == 'user' and has('Web Registration', self.userid,
664 'user'):
665 return 1
666 if not has('Edit', self.userid, self.classname):
667 return 0
668 return 1
670 def genericEditAction(self):
671 ''' Performs an edit of all of a class' items in one go.
673 The "rows" CGI var defines the CSV-formatted entries for the
674 class. New nodes are identified by the ID 'X' (or any other
675 non-existent ID) and removed lines are retired.
676 '''
677 # generic edit is per-class only
678 if not self.genericEditPermission():
679 self.error_message.append(
680 _('You do not have permission to edit %s' %self.classname))
682 # get the CSV module
683 try:
684 import csv
685 except ImportError:
686 self.error_message.append(_(
687 'Sorry, you need the csv module to use this function.<br>\n'
688 'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
689 return
691 cl = self.db.classes[self.classname]
692 idlessprops = cl.getprops(protected=0).keys()
693 props = ['id'] + idlessprops
695 # do the edit
696 rows = self.form['rows'].value.splitlines()
697 p = csv.parser()
698 found = {}
699 line = 0
700 for row in rows:
701 line += 1
702 values = p.parse(row)
703 # not a complete row, keep going
704 if not values: continue
706 # extract the nodeid
707 nodeid, values = values[0], values[1:]
708 found[nodeid] = 1
710 # confirm correct weight
711 if len(idlessprops) != len(values):
712 message=(_('Not enough values on line %(line)s'%{'line':line}))
713 return
715 # extract the new values
716 d = {}
717 for name, value in zip(idlessprops, values):
718 value = value.strip()
719 # only add the property if it has a value
720 if value:
721 # if it's a multilink, split it
722 if isinstance(cl.properties[name], hyperdb.Multilink):
723 value = value.split(':')
724 d[name] = value
726 # perform the edit
727 if cl.hasnode(nodeid):
728 # edit existing
729 cl.set(nodeid, **d)
730 else:
731 # new node
732 found[cl.create(**d)] = 1
734 # retire the removed entries
735 for nodeid in cl.list():
736 if not found.has_key(nodeid):
737 cl.retire(nodeid)
739 message = _('items edited OK')
741 # redirect to the class' edit page
742 raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname,
743 urllib.quote(message))
745 def genericEditPermission(self):
746 ''' Determine whether the user has permission to edit this class.
748 Base behaviour is to check the user can edit this class.
749 '''
750 if not self.db.security.hasPermission('Edit', self.userid,
751 self.classname):
752 return 0
753 return 1
755 def searchAction(self):
756 ''' Mangle some of the form variables.
758 Set the form ":filter" variable based on the values of the
759 filter variables - if they're set to anything other than
760 "dontcare" then add them to :filter.
761 '''
762 # generic edit is per-class only
763 if not self.searchPermission():
764 self.error_message.append(
765 _('You do not have permission to search %s' %self.classname))
767 # add a faked :filter form variable for each filtering prop
768 props = self.db.classes[self.classname].getprops()
769 for key in self.form.keys():
770 if not props.has_key(key): continue
771 if not self.form[key].value: continue
772 self.form.value.append(cgi.MiniFieldStorage(':filter', key))
774 def searchPermission(self):
775 ''' Determine whether the user has permission to search this class.
777 Base behaviour is to check the user can view this class.
778 '''
779 if not self.db.security.hasPermission('View', self.userid,
780 self.classname):
781 return 0
782 return 1
784 def XXXremove_action(self, dre=re.compile(r'([^\d]+)(\d+)')):
785 # XXX I believe this could be handled by a regular edit action that
786 # just sets the multilink...
787 # XXX handle this !
788 target = self.index_arg(':target')[0]
789 m = dre.match(target)
790 if m:
791 classname = m.group(1)
792 nodeid = m.group(2)
793 cl = self.db.getclass(classname)
794 cl.retire(nodeid)
795 # now take care of the reference
796 parentref = self.index_arg(':multilink')[0]
797 parent, prop = parentref.split(':')
798 m = dre.match(parent)
799 if m:
800 self.classname = m.group(1)
801 self.nodeid = m.group(2)
802 cl = self.db.getclass(self.classname)
803 value = cl.get(self.nodeid, prop)
804 value.remove(nodeid)
805 cl.set(self.nodeid, **{prop:value})
806 func = getattr(self, 'show%s'%self.classname)
807 return func()
808 else:
809 raise NotFound, parent
810 else:
811 raise NotFound, target
813 #
814 # Utility methods for editing
815 #
816 def _changenode(self, props):
817 ''' change the node based on the contents of the form
818 '''
819 cl = self.db.classes[self.classname]
821 # create the message
822 message, files = self._handle_message()
823 if message:
824 props['messages'] = cl.get(self.nodeid, 'messages') + [message]
825 if files:
826 props['files'] = cl.get(self.nodeid, 'files') + files
828 # make the changes
829 return cl.set(self.nodeid, **props)
831 def _createnode(self, props):
832 ''' create a node based on the contents of the form
833 '''
834 cl = self.db.classes[self.classname]
836 # check for messages and files
837 message, files = self._handle_message()
838 if message:
839 props['messages'] = [message]
840 if files:
841 props['files'] = files
842 # create the node and return it's id
843 return cl.create(**props)
845 def _handle_message(self):
846 ''' generate an edit message
847 '''
848 # handle file attachments
849 files = []
850 if self.form.has_key('__file'):
851 file = self.form['__file']
852 if file.filename:
853 filename = file.filename.split('\\')[-1]
854 mime_type = mimetypes.guess_type(filename)[0]
855 if not mime_type:
856 mime_type = "application/octet-stream"
857 # create the new file entry
858 files.append(self.db.file.create(type=mime_type,
859 name=filename, content=file.file.read()))
861 # we don't want to do a message if none of the following is true...
862 cn = self.classname
863 cl = self.db.classes[self.classname]
864 props = cl.getprops()
865 note = None
866 # in a nutshell, don't do anything if there's no note or there's no
867 # NOSY
868 if self.form.has_key('__note'):
869 note = self.form['__note'].value.strip()
870 if not note:
871 return None, files
872 if not props.has_key('messages'):
873 return None, files
874 if not isinstance(props['messages'], hyperdb.Multilink):
875 return None, files
876 if not props['messages'].classname == 'msg':
877 return None, files
878 if not (self.form.has_key('nosy') or note):
879 return None, files
881 # handle the note
882 if '\n' in note:
883 summary = re.split(r'\n\r?', note)[0]
884 else:
885 summary = note
886 m = ['%s\n'%note]
888 # handle the messageid
889 # TODO: handle inreplyto
890 messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
891 self.classname, self.instance.MAIL_DOMAIN)
893 # now create the message, attaching the files
894 content = '\n'.join(m)
895 message_id = self.db.msg.create(author=self.userid,
896 recipients=[], date=date.Date('.'), summary=summary,
897 content=content, files=files, messageid=messageid)
899 # update the messages property
900 return message_id, files
902 def _post_editnode(self, nid):
903 '''Do the linking part of the node creation.
905 If a form element has :link or :multilink appended to it, its
906 value specifies a node designator and the property on that node
907 to add _this_ node to as a link or multilink.
909 This is typically used on, eg. the file upload page to indicated
910 which issue to link the file to.
912 TODO: I suspect that this and newfile will go away now that
913 there's the ability to upload a file using the issue __file form
914 element!
915 '''
916 cn = self.classname
917 cl = self.db.classes[cn]
918 # link if necessary
919 keys = self.form.keys()
920 for key in keys:
921 if key == ':multilink':
922 value = self.form[key].value
923 if type(value) != type([]): value = [value]
924 for value in value:
925 designator, property = value.split(':')
926 link, nodeid = hyperdb.splitDesignator(designator)
927 link = self.db.classes[link]
928 # take a dupe of the list so we're not changing the cache
929 value = link.get(nodeid, property)[:]
930 value.append(nid)
931 link.set(nodeid, **{property: value})
932 elif key == ':link':
933 value = self.form[key].value
934 if type(value) != type([]): value = [value]
935 for value in value:
936 designator, property = value.split(':')
937 link, nodeid = hyperdb.splitDesignator(designator)
938 link = self.db.classes[link]
939 link.set(nodeid, **{property: nid})
942 def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
943 '''Pull properties for the given class out of the form.
944 '''
945 props = {}
946 keys = form.keys()
947 for key in keys:
948 if not cl.properties.has_key(key):
949 continue
950 proptype = cl.properties[key]
951 if isinstance(proptype, hyperdb.String):
952 value = form[key].value.strip()
953 elif isinstance(proptype, hyperdb.Password):
954 value = form[key].value.strip()
955 if not value:
956 # ignore empty password values
957 continue
958 value = password.Password(value)
959 elif isinstance(proptype, hyperdb.Date):
960 value = form[key].value.strip()
961 if value:
962 value = date.Date(form[key].value.strip())
963 else:
964 value = None
965 elif isinstance(proptype, hyperdb.Interval):
966 value = form[key].value.strip()
967 if value:
968 value = date.Interval(form[key].value.strip())
969 else:
970 value = None
971 elif isinstance(proptype, hyperdb.Link):
972 value = form[key].value.strip()
973 # see if it's the "no selection" choice
974 if value == '-1':
975 value = None
976 else:
977 # handle key values
978 link = cl.properties[key].classname
979 if not num_re.match(value):
980 try:
981 value = db.classes[link].lookup(value)
982 except KeyError:
983 raise ValueError, _('property "%(propname)s": '
984 '%(value)s not a %(classname)s')%{'propname':key,
985 'value': value, 'classname': link}
986 elif isinstance(proptype, hyperdb.Multilink):
987 value = form[key]
988 if hasattr(value, 'value'):
989 # Quite likely to be a FormItem instance
990 value = value.value
991 if not isinstance(value, type([])):
992 value = [i.strip() for i in value.split(',')]
993 else:
994 value = [i.strip() for i in value]
995 link = cl.properties[key].classname
996 l = []
997 for entry in map(str, value):
998 if entry == '': continue
999 if not num_re.match(entry):
1000 try:
1001 entry = db.classes[link].lookup(entry)
1002 except KeyError:
1003 raise ValueError, _('property "%(propname)s": '
1004 '"%(value)s" not an entry of %(classname)s')%{
1005 'propname':key, 'value': entry, 'classname': link}
1006 l.append(entry)
1007 l.sort()
1008 value = l
1009 elif isinstance(proptype, hyperdb.Boolean):
1010 value = form[key].value.strip()
1011 props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
1012 elif isinstance(proptype, hyperdb.Number):
1013 value = form[key].value.strip()
1014 props[key] = value = int(value)
1016 # get the old value
1017 if nodeid:
1018 try:
1019 existing = cl.get(nodeid, key)
1020 except KeyError:
1021 # this might be a new property for which there is no existing
1022 # value
1023 if not cl.properties.has_key(key): raise
1025 # if changed, set it
1026 if value != existing:
1027 props[key] = value
1028 else:
1029 props[key] = value
1030 return props