Code

call the cookie something else since it changed
[roundup.git] / roundup / cgi / client.py
1 # $Id: client.py,v 1.8 2002-09-03 03:23:56 richard Exp $
3 __doc__ = """
4 WWW request handler (also used in the stand-alone server).
5 """
7 import os, os.path, 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 getTemplate, HTMLRequest
14 from roundup.cgi import cgitb
16 from PageTemplates import PageTemplate
18 class Unauthorised(ValueError):
19     pass
21 class NotFound(ValueError):
22     pass
24 class Redirect(Exception):
25     pass
27 class SendFile(Exception):
28     ' Sent a file from the database '
30 class SendStaticFile(Exception):
31     ' Send a static file from the instance html directory '
33 def initialiseSecurity(security):
34     ''' Create some Permissions and Roles on the security object
36         This function is directly invoked by security.Security.__init__()
37         as a part of the Security object instantiation.
38     '''
39     security.addPermission(name="Web Registration",
40         description="User may register through the web")
41     p = security.addPermission(name="Web Access",
42         description="User may access the web interface")
43     security.addPermissionToRole('Admin', p)
45     # doing Role stuff through the web - make sure Admin can
46     p = security.addPermission(name="Web Roles",
47         description="User may manipulate user Roles through the web")
48     security.addPermissionToRole('Admin', p)
50 class Client:
51     '''
52     A note about login
53     ------------------
55     If the user has no login cookie, then they are anonymous. There
56     are two levels of anonymous use. If there is no 'anonymous' user, there
57     is no login at all and the database is opened in read-only mode. If the
58     'anonymous' user exists, the user is logged in using that user (though
59     there is no cookie). This allows them to modify the database, and all
60     modifications are attributed to the 'anonymous' user.
62     Once a user logs in, they are assigned a session. The Client instance
63     keeps the nodeid of the session as the "session" attribute.
65     Client attributes:
66         "url" is the current url path
67         "path" is the PATH_INFO inside the instance
68         "base" is the base URL for the instance
69     '''
71     def __init__(self, instance, request, env, form=None):
72         hyperdb.traceMark()
73         self.instance = instance
74         self.request = request
75         self.env = env
77         self.path = env['PATH_INFO']
78         self.split_path = self.path.split('/')
79         self.instance_path_name = env['INSTANCE_NAME']
81         # this is the base URL for this instance
82         url = self.env['SCRIPT_NAME'] + '/' + self.instance_path_name
83         self.base = urlparse.urlunparse(('http', env['HTTP_HOST'], url,
84             None, None, None))
86         # request.path is the full request path
87         x, x, path, x, x, x = urlparse.urlparse(request.path)
88         self.url = urlparse.urlunparse(('http', env['HTTP_HOST'], path,
89             None, None, None))
91         if form is None:
92             self.form = cgi.FieldStorage(environ=env)
93         else:
94             self.form = form
95         self.headers_done = 0
96         try:
97             self.debug = int(env.get("ROUNDUP_DEBUG", 0))
98         except ValueError:
99             # someone gave us a non-int debug level, turn it off
100             self.debug = 0
102     def main(self):
103         ''' Wrap the request and handle unauthorised requests
104         '''
105         self.content_action = None
106         self.ok_message = []
107         self.error_message = []
108         try:
109             # make sure we're identified (even anonymously)
110             self.determine_user()
111             # figure out the context and desired content template
112             self.determine_context()
113             # possibly handle a form submit action (may change self.message
114             # and self.template_name)
115             self.handle_action()
116             # now render the page
117             self.write(self.template('page', ok_message=self.ok_message,
118                 error_message=self.error_message))
119         except Redirect, url:
120             # let's redirect - if the url isn't None, then we need to do
121             # the headers, otherwise the headers have been set before the
122             # exception was raised
123             if url:
124                 self.header({'Location': url}, response=302)
125         except SendFile, designator:
126             self.serve_file(designator)
127         except SendStaticFile, file:
128             self.serve_static_file(file)
129         except Unauthorised, message:
130             self.write(self.template('page.unauthorised',
131                 error_message=message))
132         except:
133             # everything else
134             self.write(cgitb.html())
136     def determine_user(self):
137         ''' Determine who the user is
138         '''
139         # determine the uid to use
140         self.opendb('admin')
142         # make sure we have the session Class
143         sessions = self.db.sessions
145         # age sessions, remove when they haven't been used for a week
146         # TODO: this shouldn't be done every access
147         week = 60*60*24*7
148         now = time.time()
149         for sessid in sessions.list():
150             interval = now - sessions.get(sessid, 'last_use')
151             if interval > week:
152                 sessions.destroy(sessid)
154         # look up the user session cookie
155         cookie = Cookie.Cookie(self.env.get('HTTP_COOKIE', ''))
156         user = 'anonymous'
158         # bump the "revision" of the cookie since the format changed
159         if (cookie.has_key('roundup_user_2') and
160                 cookie['roundup_user_2'].value != 'deleted'):
162             # get the session key from the cookie
163             self.session = cookie['roundup_user_2'].value
164             # get the user from the session
165             try:
166                 # update the lifetime datestamp
167                 sessions.set(self.session, last_use=time.time())
168                 sessions.commit()
169                 user = sessions.get(self.session, 'user')
170             except KeyError:
171                 user = 'anonymous'
173         # sanity check on the user still being valid, getting the userid
174         # at the same time
175         try:
176             self.userid = self.db.user.lookup(user)
177         except (KeyError, TypeError):
178             user = 'anonymous'
180         # make sure the anonymous user is valid if we're using it
181         if user == 'anonymous':
182             self.make_user_anonymous()
183         else:
184             self.user = user
186         # reopen the database as the correct user
187         self.opendb(self.user)
189     def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
190         ''' Determine the context of this page:
192              home              (default if no url is given)
193              classname
194              designator        (classname and nodeid)
196             The desired template to be rendered is also determined There
197             are two exceptional contexts:
199              _file            - serve up a static file
200              path len > 1     - serve up a FileClass content
201                                 (the additional path gives the browser a
202                                  nicer filename to save as)
204             The template used is specified by the :template CGI variable,
205             which defaults to:
206              only classname suplied:          "index"
207              full item designator supplied:   "item"
209             We set:
210              self.classname
211              self.nodeid
212              self.template_name
213         '''
214         # default the optional variables
215         self.classname = None
216         self.nodeid = None
218         # determine the classname and possibly nodeid
219         path = self.split_path
220         if not path or path[0] in ('', 'home', 'index'):
221             if self.form.has_key(':template'):
222                 self.template_type = self.form[':template'].value
223                 self.template_name = 'home' + '.' + self.template_type
224             else:
225                 self.template_type = ''
226                 self.template_name = 'home'
227             return
228         elif path[0] == '_file':
229             raise SendStaticFile, path[1]
230         else:
231             self.classname = path[0]
232             if len(path) > 1:
233                 # send the file identified by the designator in path[0]
234                 raise SendFile, path[0]
236         # see if we got a designator
237         m = dre.match(self.classname)
238         if m:
239             self.classname = m.group(1)
240             self.nodeid = m.group(2)
241             # with a designator, we default to item view
242             self.template_type = 'item'
243         else:
244             # with only a class, we default to index view
245             self.template_type = 'index'
247         # see if we have a template override
248         if self.form.has_key(':template'):
249             self.template_type = self.form[':template'].value
252         # see if we were passed in a message
253         if self.form.has_key(':ok_message'):
254             self.ok_message.append(self.form[':ok_message'].value)
255         if self.form.has_key(':error_message'):
256             self.error_message.append(self.form[':error_message'].value)
258         # we have the template name now
259         self.template_name = self.classname + '.' + self.template_type
261     def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
262         ''' Serve the file from the content property of the designated item.
263         '''
264         m = dre.match(str(designator))
265         if not m:
266             raise NotFound, str(designator)
267         classname, nodeid = m.group(1), m.group(2)
268         if classname != 'file':
269             raise NotFound, designator
271         # we just want to serve up the file named
272         file = self.db.file
273         self.header({'Content-Type': file.get(nodeid, 'type')})
274         self.write(file.get(nodeid, 'content'))
276     def serve_static_file(self, file):
277         # we just want to serve up the file named
278         mt = mimetypes.guess_type(str(file))[0]
279         self.header({'Content-Type': mt})
280         self.write(open(os.path.join(self.instance.TEMPLATES, file)).read())
282     def template(self, name, **kwargs):
283         ''' Return a PageTemplate for the named page
284         '''
285         pt = getTemplate(self.instance.TEMPLATES, name)
286         # XXX handle PT rendering errors here more nicely
287         try:
288             # let the template render figure stuff out
289             return pt.render(self, None, None, **kwargs)
290         except PageTemplate.PTRuntimeError, message:
291             return '<strong>%s</strong><ol>%s</ol>'%(message,
292                 '<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         'register': 'registerAction',
310         'login':    'login_action',
311         'logout':   'logout_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              "register"  -> self.registerAction
323              "login"     -> self.login_action
324              "logout"    -> self.logout_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_2=%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_2=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_2=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 registerAction(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         # create the new user
482         cl = self.db.user
484         # parse the props from the form
485         try:
486             props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
487         except (ValueError, KeyError), message:
488             self.error_message.append(_('Error: ') + str(message))
489             return
491         # make sure we're allowed to register
492         if not self.registerPermission(props):
493             raise Unauthorised, _("You do not have permission to register")
495         # re-open the database as "admin"
496         if self.user != 'admin':
497             self.opendb('admin')
498             
499         # create the new user
500         cl = self.db.user
501         try:
502             props = parsePropsFromForm(self.db, cl, self.form)
503             props['roles'] = self.instance.NEW_WEB_USER_ROLES
504             self.userid = cl.create(**props)
505             self.db.commit()
506         except ValueError, message:
507             self.error_message.append(message)
509         # log the new user in
510         self.user = cl.get(self.userid, 'username')
511         # re-open the database for real, using the user
512         self.opendb(self.user)
513         password = self.db.user.get(self.userid, 'password')
514         self.set_cookie(self.user, password)
516         # nice message
517         self.ok_message.append(_('You are now registered, welcome!'))
519     def registerPermission(self, props):
520         ''' Determine whether the user has permission to register
522             Base behaviour is to check the user has "Web Registration".
523         '''
524         # registration isn't allowed to supply roles
525         if props.has_key('roles'):
526             return 0
527         if self.db.security.hasPermission('Web Registration', self.userid):
528             return 1
529         return 0
531     def editItemAction(self):
532         ''' Perform an edit of an item in the database.
534             Some special form elements:
536             :link=designator:property
537             :multilink=designator:property
538              The value specifies a node designator and the property on that
539              node to add _this_ node to as a link or multilink.
540             __note
541              Create a message and attach it to the current node's
542              "messages" property.
543             __file
544              Create a file and attach it to the current node's
545              "files" property. Attach the file to the message created from
546              the __note if it's supplied.
547         '''
548         cl = self.db.classes[self.classname]
550         # parse the props from the form
551         try:
552             props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
553         except (ValueError, KeyError), message:
554             self.error_message.append(_('Error: ') + str(message))
555             return
557         # check permission
558         if not self.editItemPermission(props):
559             self.error_message.append(
560                 _('You do not have permission to edit %(classname)s'%
561                 self.__dict__))
562             return
564         # perform the edit
565         try:
566             # make changes to the node
567             props = self._changenode(props)
568             # handle linked nodes 
569             self._post_editnode(self.nodeid)
570         except (ValueError, KeyError), message:
571             self.error_message.append(_('Error: ') + str(message))
572             return
574         # commit now that all the tricky stuff is done
575         self.db.commit()
577         # and some nice feedback for the user
578         if props:
579             message = _('%(changes)s edited ok')%{'changes':
580                 ', '.join(props.keys())}
581         elif self.form.has_key('__note') and self.form['__note'].value:
582             message = _('note added')
583         elif (self.form.has_key('__file') and self.form['__file'].filename):
584             message = _('file added')
585         else:
586             message = _('nothing changed')
588         # redirect to the item's edit page
589         raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
590             self.nodeid,  urllib.quote(message))
592     def editItemPermission(self, props):
593         ''' Determine whether the user has permission to edit this item.
595             Base behaviour is to check the user can edit this class. If we're
596             editing the "user" class, users are allowed to edit their own
597             details. Unless it's the "roles" property, which requires the
598             special Permission "Web Roles".
599         '''
600         # if this is a user node and the user is editing their own node, then
601         # we're OK
602         has = self.db.security.hasPermission
603         if self.classname == 'user':
604             # reject if someone's trying to edit "roles" and doesn't have the
605             # right permission.
606             if props.has_key('roles') and not has('Web Roles', self.userid,
607                     'user'):
608                 return 0
609             # if the item being edited is the current user, we're ok
610             if self.nodeid == self.userid:
611                 return 1
612         if self.db.security.hasPermission('Edit', self.userid, self.classname):
613             return 1
614         return 0
616     def newItemAction(self):
617         ''' Add a new item to the database.
619             This follows the same form as the editItemAction
620         '''
621         cl = self.db.classes[self.classname]
623         # parse the props from the form
624         try:
625             props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
626         except (ValueError, KeyError), message:
627             self.error_message.append(_('Error: ') + str(message))
628             return
630         if not self.newItemPermission(props):
631             self.error_message.append(
632                 _('You do not have permission to create %s' %self.classname))
634         # XXX
635 #        cl = self.db.classes[cn]
636 #        if self.form.has_key(':multilink'):
637 #            link = self.form[':multilink'].value
638 #            designator, linkprop = link.split(':')
639 #            xtra = ' for <a href="%s">%s</a>' % (designator, designator)
640 #        else:
641 #            xtra = ''
643         try:
644             # do the create
645             nid = self._createnode(props)
647             # handle linked nodes 
648             self._post_editnode(nid)
650             # commit now that all the tricky stuff is done
651             self.db.commit()
653             # render the newly created item
654             self.nodeid = nid
656             # and some nice feedback for the user
657             message = _('%(classname)s created ok')%self.__dict__
658         except (ValueError, KeyError), message:
659             self.error_message.append(_('Error: ') + str(message))
660             return
661         except:
662             # oops
663             self.db.rollback()
664             s = StringIO.StringIO()
665             traceback.print_exc(None, s)
666             self.error_message.append('<pre>%s</pre>'%cgi.escape(s.getvalue()))
667             return
669         # redirect to the new item's page
670         raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
671             nid,  urllib.quote(message))
673     def newItemPermission(self, props):
674         ''' Determine whether the user has permission to create (edit) this
675             item.
677             Base behaviour is to check the user can edit this class. No
678             additional property checks are made. Additionally, new user items
679             may be created if the user has the "Web Registration" Permission.
680         '''
681         has = self.db.security.hasPermission
682         if self.classname == 'user' and has('Web Registration', self.userid,
683                 'user'):
684             return 1
685         if has('Edit', self.userid, self.classname):
686             return 1
687         return 0
689     def genericEditAction(self):
690         ''' Performs an edit of all of a class' items in one go.
692             The "rows" CGI var defines the CSV-formatted entries for the
693             class. New nodes are identified by the ID 'X' (or any other
694             non-existent ID) and removed lines are retired.
695         '''
696         # generic edit is per-class only
697         if not self.genericEditPermission():
698             self.error_message.append(
699                 _('You do not have permission to edit %s' %self.classname))
701         # get the CSV module
702         try:
703             import csv
704         except ImportError:
705             self.error_message.append(_(
706                 'Sorry, you need the csv module to use this function.<br>\n'
707                 'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
708             return
710         cl = self.db.classes[self.classname]
711         idlessprops = cl.getprops(protected=0).keys()
712         props = ['id'] + idlessprops
714         # do the edit
715         rows = self.form['rows'].value.splitlines()
716         p = csv.parser()
717         found = {}
718         line = 0
719         for row in rows:
720             line += 1
721             values = p.parse(row)
722             # not a complete row, keep going
723             if not values: continue
725             # extract the nodeid
726             nodeid, values = values[0], values[1:]
727             found[nodeid] = 1
729             # confirm correct weight
730             if len(idlessprops) != len(values):
731                 message=(_('Not enough values on line %(line)s'%{'line':line}))
732                 return
734             # extract the new values
735             d = {}
736             for name, value in zip(idlessprops, values):
737                 value = value.strip()
738                 # only add the property if it has a value
739                 if value:
740                     # if it's a multilink, split it
741                     if isinstance(cl.properties[name], hyperdb.Multilink):
742                         value = value.split(':')
743                     d[name] = value
745             # perform the edit
746             if cl.hasnode(nodeid):
747                 # edit existing
748                 cl.set(nodeid, **d)
749             else:
750                 # new node
751                 found[cl.create(**d)] = 1
753         # retire the removed entries
754         for nodeid in cl.list():
755             if not found.has_key(nodeid):
756                 cl.retire(nodeid)
758         message = _('items edited OK')
760         # redirect to the class' edit page
761         raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname, 
762             urllib.quote(message))
764     def genericEditPermission(self):
765         ''' Determine whether the user has permission to edit this class.
767             Base behaviour is to check the user can edit this class.
768         ''' 
769         if not self.db.security.hasPermission('Edit', self.userid,
770                 self.classname):
771             return 0
772         return 1
774     def searchAction(self):
775         ''' Mangle some of the form variables.
777             Set the form ":filter" variable based on the values of the
778             filter variables - if they're set to anything other than
779             "dontcare" then add them to :filter.
781             Also handle the ":queryname" variable and save off the query to
782             the user's query list.
783         '''
784         # generic edit is per-class only
785         if not self.searchPermission():
786             self.error_message.append(
787                 _('You do not have permission to search %s' %self.classname))
789         # add a faked :filter form variable for each filtering prop
790         props = self.db.classes[self.classname].getprops()
791         for key in self.form.keys():
792             if not props.has_key(key): continue
793             if not self.form[key].value: continue
794             self.form.value.append(cgi.MiniFieldStorage(':filter', key))
796         # handle saving the query params
797         if self.form.has_key(':queryname'):
798             queryname = self.form[':queryname'].value.strip()
799             if queryname:
800                 # parse the environment and figure what the query _is_
801                 req = HTMLRequest(self)
802                 url = req.indexargs_href('', {})
804                 # handle editing an existing query
805                 try:
806                     qid = self.db.query.lookup(queryname)
807                     self.db.query.set(qid, klass=self.classname, url=url)
808                 except KeyError:
809                     # create a query
810                     qid = self.db.query.create(name=queryname,
811                         klass=self.classname, url=url)
813                     # and add it to the user's query multilink
814                     queries = self.db.user.get(self.userid, 'queries')
815                     queries.append(qid)
816                     self.db.user.set(self.userid, queries=queries)
818                 # commit the query change to the database
819                 self.db.commit()
822     def searchPermission(self):
823         ''' Determine whether the user has permission to search this class.
825             Base behaviour is to check the user can view this class.
826         ''' 
827         if not self.db.security.hasPermission('View', self.userid,
828                 self.classname):
829             return 0
830         return 1
832     def XXXremove_action(self,  dre=re.compile(r'([^\d]+)(\d+)')):
833         # XXX I believe this could be handled by a regular edit action that
834         # just sets the multilink...
835         # XXX handle this !
836         target = self.index_arg(':target')[0]
837         m = dre.match(target)
838         if m:
839             classname = m.group(1)
840             nodeid = m.group(2)
841             cl = self.db.getclass(classname)
842             cl.retire(nodeid)
843             # now take care of the reference
844             parentref =  self.index_arg(':multilink')[0]
845             parent, prop = parentref.split(':')
846             m = dre.match(parent)
847             if m:
848                 self.classname = m.group(1)
849                 self.nodeid = m.group(2)
850                 cl = self.db.getclass(self.classname)
851                 value = cl.get(self.nodeid, prop)
852                 value.remove(nodeid)
853                 cl.set(self.nodeid, **{prop:value})
854                 func = getattr(self, 'show%s'%self.classname)
855                 return func()
856             else:
857                 raise NotFound, parent
858         else:
859             raise NotFound, target
861     #
862     #  Utility methods for editing
863     #
864     def _changenode(self, props):
865         ''' change the node based on the contents of the form
866         '''
867         cl = self.db.classes[self.classname]
869         # create the message
870         message, files = self._handle_message()
871         if message:
872             props['messages'] = cl.get(self.nodeid, 'messages') + [message]
873         if files:
874             props['files'] = cl.get(self.nodeid, 'files') + files
876         # make the changes
877         return cl.set(self.nodeid, **props)
879     def _createnode(self, props):
880         ''' create a node based on the contents of the form
881         '''
882         cl = self.db.classes[self.classname]
884         # check for messages and files
885         message, files = self._handle_message()
886         if message:
887             props['messages'] = [message]
888         if files:
889             props['files'] = files
890         # create the node and return it's id
891         return cl.create(**props)
893     def _handle_message(self):
894         ''' generate an edit message
895         '''
896         # handle file attachments 
897         files = []
898         if self.form.has_key('__file'):
899             file = self.form['__file']
900             if file.filename:
901                 filename = file.filename.split('\\')[-1]
902                 mime_type = mimetypes.guess_type(filename)[0]
903                 if not mime_type:
904                     mime_type = "application/octet-stream"
905                 # create the new file entry
906                 files.append(self.db.file.create(type=mime_type,
907                     name=filename, content=file.file.read()))
909         # we don't want to do a message if none of the following is true...
910         cn = self.classname
911         cl = self.db.classes[self.classname]
912         props = cl.getprops()
913         note = None
914         # in a nutshell, don't do anything if there's no note or there's no
915         # NOSY
916         if self.form.has_key('__note'):
917             note = self.form['__note'].value.strip()
918         if not note:
919             return None, files
920         if not props.has_key('messages'):
921             return None, files
922         if not isinstance(props['messages'], hyperdb.Multilink):
923             return None, files
924         if not props['messages'].classname == 'msg':
925             return None, files
926         if not (self.form.has_key('nosy') or note):
927             return None, files
929         # handle the note
930         if '\n' in note:
931             summary = re.split(r'\n\r?', note)[0]
932         else:
933             summary = note
934         m = ['%s\n'%note]
936         # handle the messageid
937         # TODO: handle inreplyto
938         messageid = "<%s.%s.%s@%s>"%(time.time(), random.random(),
939             self.classname, self.instance.MAIL_DOMAIN)
941         # now create the message, attaching the files
942         content = '\n'.join(m)
943         message_id = self.db.msg.create(author=self.userid,
944             recipients=[], date=date.Date('.'), summary=summary,
945             content=content, files=files, messageid=messageid)
947         # update the messages property
948         return message_id, files
950     def _post_editnode(self, nid):
951         '''Do the linking part of the node creation.
953            If a form element has :link or :multilink appended to it, its
954            value specifies a node designator and the property on that node
955            to add _this_ node to as a link or multilink.
957            This is typically used on, eg. the file upload page to indicated
958            which issue to link the file to.
960            TODO: I suspect that this and newfile will go away now that
961            there's the ability to upload a file using the issue __file form
962            element!
963         '''
964         cn = self.classname
965         cl = self.db.classes[cn]
966         # link if necessary
967         keys = self.form.keys()
968         for key in keys:
969             if key == ':multilink':
970                 value = self.form[key].value
971                 if type(value) != type([]): value = [value]
972                 for value in value:
973                     designator, property = value.split(':')
974                     link, nodeid = hyperdb.splitDesignator(designator)
975                     link = self.db.classes[link]
976                     # take a dupe of the list so we're not changing the cache
977                     value = link.get(nodeid, property)[:]
978                     value.append(nid)
979                     link.set(nodeid, **{property: value})
980             elif key == ':link':
981                 value = self.form[key].value
982                 if type(value) != type([]): value = [value]
983                 for value in value:
984                     designator, property = value.split(':')
985                     link, nodeid = hyperdb.splitDesignator(designator)
986                     link = self.db.classes[link]
987                     link.set(nodeid, **{property: nid})
990 def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
991     '''Pull properties for the given class out of the form.
992     '''
993     props = {}
994     keys = form.keys()
995     for key in keys:
996         if not cl.properties.has_key(key):
997             continue
998         proptype = cl.properties[key]
999         if isinstance(proptype, hyperdb.String):
1000             value = form[key].value.strip()
1001         elif isinstance(proptype, hyperdb.Password):
1002             value = form[key].value.strip()
1003             if not value:
1004                 # ignore empty password values
1005                 continue
1006             value = password.Password(value)
1007         elif isinstance(proptype, hyperdb.Date):
1008             value = form[key].value.strip()
1009             if value:
1010                 value = date.Date(form[key].value.strip())
1011             else:
1012                 value = None
1013         elif isinstance(proptype, hyperdb.Interval):
1014             value = form[key].value.strip()
1015             if value:
1016                 value = date.Interval(form[key].value.strip())
1017             else:
1018                 value = None
1019         elif isinstance(proptype, hyperdb.Link):
1020             value = form[key].value.strip()
1021             # see if it's the "no selection" choice
1022             if value == '-1':
1023                 value = None
1024             else:
1025                 # handle key values
1026                 link = cl.properties[key].classname
1027                 if not num_re.match(value):
1028                     try:
1029                         value = db.classes[link].lookup(value)
1030                     except KeyError:
1031                         raise ValueError, _('property "%(propname)s": '
1032                             '%(value)s not a %(classname)s')%{'propname':key, 
1033                             'value': value, 'classname': link}
1034         elif isinstance(proptype, hyperdb.Multilink):
1035             value = form[key]
1036             if not isinstance(value, type([])):
1037                 value = [i.strip() for i in value.split(',')]
1038             else:
1039                 value = [i.value.strip() for i in value]
1040             link = cl.properties[key].classname
1041             l = []
1042             for entry in map(str, value):
1043                 if entry == '': continue
1044                 if not num_re.match(entry):
1045                     try:
1046                         entry = db.classes[link].lookup(entry)
1047                     except KeyError:
1048                         raise ValueError, _('property "%(propname)s": '
1049                             '"%(value)s" not an entry of %(classname)s')%{
1050                             'propname':key, 'value': entry, 'classname': link}
1051                 l.append(entry)
1052             l.sort()
1053             value = l
1054         elif isinstance(proptype, hyperdb.Boolean):
1055             value = form[key].value.strip()
1056             props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
1057         elif isinstance(proptype, hyperdb.Number):
1058             value = form[key].value.strip()
1059             props[key] = value = int(value)
1061         # get the old value
1062         if nodeid:
1063             try:
1064                 existing = cl.get(nodeid, key)
1065             except KeyError:
1066                 # this might be a new property for which there is no existing
1067                 # value
1068                 if not cl.properties.has_key(key): raise
1070             # if changed, set it
1071             if value != existing:
1072                 props[key] = value
1073         else:
1074             props[key] = value
1075     return props