Code

add and use Reject exception (sf bug 700265)
[roundup.git] / roundup / cgi / actions.py
1 #$Id: actions.py,v 1.15 2004-03-26 00:44:11 richard Exp $
3 import re, cgi, StringIO, urllib, Cookie, time, random
5 from roundup import hyperdb, token, date, password, rcsv
6 from roundup.i18n import _
7 from roundup.cgi import templating
8 from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError
9 from roundup.mailgw import uidFromAddress
11 __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
12            'EditCSVAction', 'EditItemAction', 'PassResetAction',
13            'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
14            'NewItemAction', 'ExportCSVAction']
16 # used by a couple of routines
17 chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
19 class Action:
20     def __init__(self, client):
21         self.client = client
22         self.form = client.form
23         self.db = client.db
24         self.nodeid = client.nodeid
25         self.template = client.template
26         self.classname = client.classname
27         self.userid = client.userid
28         self.base = client.base
29         self.user = client.user
31     def execute(self):
32         """Execute the action specified by this object."""
33         self.permission()
34         self.handle()
36     name = ''
37     permissionType = None
38     def permission(self):
39         """Check whether the user has permission to execute this action.
41         True by default. If the permissionType attribute is a string containing
42         a simple permission, check whether the user has that permission.
43         Subclasses must also define the name attribute if they define
44         permissionType.
46         Despite having this permission, users may still be unauthorised to
47         perform parts of actions. It is up to the subclasses to detect this.
48         """
49         if (self.permissionType and
50                 not self.hasPermission(self.permissionType)):
51             info = {'action': self.name, 'classname': self.classname}
52             raise Unauthorised, _('You do not have permission to '
53                 '%(action)s the %(classname)s class.')%info
55     def hasPermission(self, permission):
56         """Check whether the user has 'permission' on the current class."""
57         return self.db.security.hasPermission(permission, self.client.userid,
58             self.client.classname)
60 class ShowAction(Action):
61     def handle(self, typere=re.compile('[@:]type'),
62                numre=re.compile('[@:]number')):
63         """Show a node of a particular class/id."""
64         t = n = ''
65         for key in self.form.keys():
66             if typere.match(key):
67                 t = self.form[key].value.strip()
68             elif numre.match(key):
69                 n = self.form[key].value.strip()
70         if not t:
71             raise ValueError, 'No type specified'
72         if not n:
73             raise SeriousError, _('No ID entered')
74         try:
75             int(n)
76         except ValueError:
77             d = {'input': n, 'classname': t}
78             raise SeriousError, _(
79                 '"%(input)s" is not an ID (%(classname)s ID required)')%d
80         url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n)
81         raise Redirect, url
83 class RetireAction(Action):
84     name = 'retire'
85     permissionType = 'Edit'
87     def handle(self):
88         """Retire the context item."""
89         # if we want to view the index template now, then unset the nodeid
90         # context info (a special-case for retire actions on the index page)
91         nodeid = self.nodeid
92         if self.template == 'index':
93             self.client.nodeid = None
95         # make sure we don't try to retire admin or anonymous
96         if self.classname == 'user' and \
97                 self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
98             raise ValueError, _('You may not retire the admin or anonymous user')
100         # do the retire
101         self.db.getclass(self.classname).retire(nodeid)
102         self.db.commit()
104         self.client.ok_message.append(
105             _('%(classname)s %(itemid)s has been retired')%{
106                 'classname': self.classname.capitalize(), 'itemid': nodeid})
108 class SearchAction(Action):
109     name = 'search'
110     permissionType = 'View'
112     def handle(self, wcre=re.compile(r'[\s,]+')):
113         """Mangle some of the form variables.
115         Set the form ":filter" variable based on the values of the filter
116         variables - if they're set to anything other than "dontcare" then add
117         them to :filter.
119         Handle the ":queryname" variable and save off the query to the user's
120         query list.
122         Split any String query values on whitespace and comma.
124         """
125         self.fakeFilterVars()
126         queryname = self.getQueryName()
128         # handle saving the query params
129         if queryname:
130             # parse the environment and figure what the query _is_
131             req = templating.HTMLRequest(self.client)
133             # The [1:] strips off the '?' character, it isn't part of the
134             # query string.
135             url = req.indexargs_href('', {})[1:]
137             # handle editing an existing query
138             try:
139                 qid = self.db.query.lookup(queryname)
140                 self.db.query.set(qid, klass=self.classname, url=url)
141             except KeyError:
142                 # create a query
143                 qid = self.db.query.create(name=queryname,
144                     klass=self.classname, url=url)
146             # and add it to the user's query multilink
147             queries = self.db.user.get(self.userid, 'queries')
148             if qid not in queries:
149                 queries.append(qid)
150                 self.db.user.set(self.userid, queries=queries)
152             # commit the query change to the database
153             self.db.commit()
155     def fakeFilterVars(self):
156         """Add a faked :filter form variable for each filtering prop."""
157         props = self.db.classes[self.classname].getprops()
158         for key in self.form.keys():
159             if not props.has_key(key):
160                 continue
161             if isinstance(self.form[key], type([])):
162                 # search for at least one entry which is not empty
163                 for minifield in self.form[key]:
164                     if minifield.value:
165                         break
166                 else:
167                     continue
168             else:
169                 if not self.form[key].value:
170                     continue
171                 if isinstance(props[key], hyperdb.String):
172                     v = self.form[key].value
173                     l = token.token_split(v)
174                     if len(l) > 1 or l[0] != v:
175                         self.form.value.remove(self.form[key])
176                         # replace the single value with the split list
177                         for v in l:
178                             self.form.value.append(cgi.MiniFieldStorage(key, v))
180             self.form.value.append(cgi.MiniFieldStorage('@filter', key))
182     FV_QUERYNAME = re.compile(r'[@:]queryname')
183     def getQueryName(self):
184         for key in self.form.keys():
185             if self.FV_QUERYNAME.match(key):
186                 return self.form[key].value.strip()
187         return ''
189 class EditCSVAction(Action):
190     name = 'edit'
191     permissionType = 'Edit'
193     def handle(self):
194         """Performs an edit of all of a class' items in one go.
196         The "rows" CGI var defines the CSV-formatted entries for the class. New
197         nodes are identified by the ID 'X' (or any other non-existent ID) and
198         removed lines are retired.
200         """
201         # get the CSV module
202         if rcsv.error:
203             self.client.error_message.append(_(rcsv.error))
204             return
206         cl = self.db.classes[self.classname]
207         idlessprops = cl.getprops(protected=0).keys()
208         idlessprops.sort()
209         props = ['id'] + idlessprops
211         # do the edit
212         rows = StringIO.StringIO(self.form['rows'].value)
213         reader = rcsv.reader(rows, rcsv.comma_separated)
214         found = {}
215         line = 0
216         for values in reader:
217             line += 1
218             if line == 1: continue
219             # skip property names header
220             if values == props:
221                 continue
223             # extract the nodeid
224             nodeid, values = values[0], values[1:]
225             found[nodeid] = 1
227             # see if the node exists
228             if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
229                 exists = 0
230             else:
231                 exists = 1
233             # confirm correct weight
234             if len(idlessprops) != len(values):
235                 self.client.error_message.append(
236                     _('Not enough values on line %(line)s')%{'line':line})
237                 return
239             # extract the new values
240             d = {}
241             for name, value in zip(idlessprops, values):
242                 prop = cl.properties[name]
243                 value = value.strip()
244                 # only add the property if it has a value
245                 if value:
246                     # if it's a multilink, split it
247                     if isinstance(prop, hyperdb.Multilink):
248                         value = value.split(':')
249                     elif isinstance(prop, hyperdb.Password):
250                         value = password.Password(value)
251                     elif isinstance(prop, hyperdb.Interval):
252                         value = date.Interval(value)
253                     elif isinstance(prop, hyperdb.Date):
254                         value = date.Date(value)
255                     elif isinstance(prop, hyperdb.Boolean):
256                         value = value.lower() in ('yes', 'true', 'on', '1')
257                     elif isinstance(prop, hyperdb.Number):
258                         value = float(value)
259                     d[name] = value
260                 elif exists:
261                     # nuke the existing value
262                     if isinstance(prop, hyperdb.Multilink):
263                         d[name] = []
264                     else:
265                         d[name] = None
267             # perform the edit
268             if exists:
269                 # edit existing
270                 cl.set(nodeid, **d)
271             else:
272                 # new node
273                 found[cl.create(**d)] = 1
275         # retire the removed entries
276         for nodeid in cl.list():
277             if not found.has_key(nodeid):
278                 cl.retire(nodeid)
280         # all OK
281         self.db.commit()
283         self.client.ok_message.append(_('Items edited OK'))
285 class _EditAction(Action):
286     def isEditingSelf(self):
287         """Check whether a user is editing his/her own details."""
288         return (self.nodeid == self.userid
289                 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
291     def editItemPermission(self, props):
292         """Determine whether the user has permission to edit this item.
294         Base behaviour is to check the user can edit this class. If we're
295         editing the "user" class, users are allowed to edit their own details.
296         Unless it's the "roles" property, which requires the special Permission
297         "Web Roles".
298         """
299         if self.classname == 'user':
300             if props.has_key('roles') and not self.hasPermission('Web Roles'):
301                 raise Unauthorised, _("You do not have permission to edit user roles")
302             if self.isEditingSelf():
303                 return 1
304         if self.hasPermission('Edit'):
305             return 1
306         return 0
308     def newItemPermission(self, props):
309         """Determine whether the user has permission to create (edit) this item.
311         Base behaviour is to check the user can edit this class. No additional
312         property checks are made. Additionally, new user items may be created
313         if the user has the "Web Registration" Permission.
315         """
316         if (self.classname == 'user' and self.hasPermission('Web Registration')
317             or self.hasPermission('Edit')):
318             return 1
319         return 0
321     #
322     #  Utility methods for editing
323     #
324     def _editnodes(self, all_props, all_links, newids=None):
325         ''' Use the props in all_props to perform edit and creation, then
326             use the link specs in all_links to do linking.
327         '''
328         # figure dependencies and re-work links
329         deps = {}
330         links = {}
331         for cn, nodeid, propname, vlist in all_links:
332             if not all_props.has_key((cn, nodeid)):
333                 # link item to link to doesn't (and won't) exist
334                 continue
335             for value in vlist:
336                 if not all_props.has_key(value):
337                     # link item to link to doesn't (and won't) exist
338                     continue
339                 deps.setdefault((cn, nodeid), []).append(value)
340                 links.setdefault(value, []).append((cn, nodeid, propname))
342         # figure chained dependencies ordering
343         order = []
344         done = {}
345         # loop detection
346         change = 0
347         while len(all_props) != len(done):
348             for needed in all_props.keys():
349                 if done.has_key(needed):
350                     continue
351                 tlist = deps.get(needed, [])
352                 for target in tlist:
353                     if not done.has_key(target):
354                         break
355                 else:
356                     done[needed] = 1
357                     order.append(needed)
358                     change = 1
359             if not change:
360                 raise ValueError, 'linking must not loop!'
362         # now, edit / create
363         m = []
364         for needed in order:
365             props = all_props[needed]
366             if not props:
367                 # nothing to do
368                 continue
369             cn, nodeid = needed
371             if nodeid is not None and int(nodeid) > 0:
372                 # make changes to the node
373                 props = self._changenode(cn, nodeid, props)
375                 # and some nice feedback for the user
376                 if props:
377                     info = ', '.join(props.keys())
378                     m.append('%s %s %s edited ok'%(cn, nodeid, info))
379                 else:
380                     m.append('%s %s - nothing changed'%(cn, nodeid))
381             else:
382                 assert props
384                 # make a new node
385                 newid = self._createnode(cn, props)
386                 if nodeid is None:
387                     self.nodeid = newid
388                 nodeid = newid
390                 # and some nice feedback for the user
391                 m.append('%s %s created'%(cn, newid))
393             # fill in new ids in links
394             if links.has_key(needed):
395                 for linkcn, linkid, linkprop in links[needed]:
396                     props = all_props[(linkcn, linkid)]
397                     cl = self.db.classes[linkcn]
398                     propdef = cl.getprops()[linkprop]
399                     if not props.has_key(linkprop):
400                         if linkid is None or linkid.startswith('-'):
401                             # linking to a new item
402                             if isinstance(propdef, hyperdb.Multilink):
403                                 props[linkprop] = [newid]
404                             else:
405                                 props[linkprop] = newid
406                         else:
407                             # linking to an existing item
408                             if isinstance(propdef, hyperdb.Multilink):
409                                 existing = cl.get(linkid, linkprop)[:]
410                                 existing.append(nodeid)
411                                 props[linkprop] = existing
412                             else:
413                                 props[linkprop] = newid
415         return '<br>'.join(m)
417     def _changenode(self, cn, nodeid, props):
418         """Change the node based on the contents of the form."""
419         # check for permission
420         if not self.editItemPermission(props):
421             raise Unauthorised, 'You do not have permission to edit %s'%cn
423         # make the changes
424         cl = self.db.classes[cn]
425         return cl.set(nodeid, **props)
427     def _createnode(self, cn, props):
428         """Create a node based on the contents of the form."""
429         # check for permission
430         if not self.newItemPermission(props):
431             raise Unauthorised, 'You do not have permission to create %s'%cn
433         # create the node and return its id
434         cl = self.db.classes[cn]
435         return cl.create(**props)
437 class EditItemAction(_EditAction):
438     def lastUserActivity(self):
439         if self.form.has_key(':lastactivity'):
440             return date.Date(self.form[':lastactivity'].value)
441         elif self.form.has_key('@lastactivity'):
442             return date.Date(self.form['@lastactivity'].value)
443         else:
444             return None
446     def lastNodeActivity(self):
447         cl = getattr(self.client.db, self.classname)
448         return cl.get(self.nodeid, 'activity')
450     def detectCollision(self, userActivity, nodeActivity):
451         # Result from lastUserActivity may be None. If it is, assume there's no
452         # conflict, or at least not one we can detect.
453         if userActivity:
454             return userActivity < nodeActivity
456     def handleCollision(self):
457         self.client.template = 'collision'
459     def handle(self):
460         """Perform an edit of an item in the database.
462         See parsePropsFromForm and _editnodes for special variables.
464         """
465         if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
466             self.handleCollision()
467             return
469         props, links = self.client.parsePropsFromForm()
471         # handle the props
472         try:
473             message = self._editnodes(props, links)
474         except (ValueError, KeyError, IndexError), message:
475             self.client.error_message.append(_('Apply Error: ') + str(message))
476             return
478         # commit now that all the tricky stuff is done
479         self.db.commit()
481         # redirect to the item's edit page
482         # redirect to finish off
483         url = self.base + self.classname
484         # note that this action might have been called by an index page, so
485         # we will want to include index-page args in this URL too
486         if self.nodeid is not None:
487             url += self.nodeid
488         url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
489             urllib.quote(self.template))
490         if self.nodeid is None:
491             req = templating.HTMLRequest(self)
492             url += '&' + req.indexargs_href('', {})[1:]
493         raise Redirect, url
495 class NewItemAction(_EditAction):
496     def handle(self):
497         ''' Add a new item to the database.
499             This follows the same form as the EditItemAction, with the same
500             special form values.
501         '''
502         # parse the props from the form
503         try:
504             props, links = self.client.parsePropsFromForm(create=1)
505         except (ValueError, KeyError), message:
506             self.client.error_message.append(_('Error: ') + str(message))
507             return
509         # handle the props - edit or create
510         try:
511             # when it hits the None element, it'll set self.nodeid
512             messages = self._editnodes(props, links)
514         except (ValueError, KeyError, IndexError), message:
515             # these errors might just be indicative of user dumbness
516             self.client.error_message.append(_('Error: ') + str(message))
517             return
519         # commit now that all the tricky stuff is done
520         self.db.commit()
522         # redirect to the new item's page
523         raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
524             self.classname, self.nodeid, urllib.quote(messages),
525             urllib.quote(self.template))
527 class PassResetAction(Action):
528     def handle(self):
529         """Handle password reset requests.
531         Presence of either "name" or "address" generates email. Presence of
532         "otk" performs the reset.
534         """
535         if self.form.has_key('otk'):
536             # pull the rego information out of the otk database
537             otk = self.form['otk'].value
538             otks = self.db.getOTKManager()
539             uid = otks.get(otk, 'uid')
540             if uid is None:
541                 self.client.error_message.append("""Invalid One Time Key!
542 (a Mozilla bug may cause this message to show up erroneously,
543  please check your email)""")
544                 return
546             # re-open the database as "admin"
547             if self.user != 'admin':
548                 self.client.opendb('admin')
549                 self.db = self.client.db
551             # change the password
552             newpw = password.generatePassword()
554             cl = self.db.user
555             # XXX we need to make the "default" page be able to display errors!
556             try:
557                 # set the password
558                 cl.set(uid, password=password.Password(newpw))
559                 # clear the props from the otk database
560                 otks.destroy(otk)
561                 self.db.commit()
562             except (ValueError, KeyError), message:
563                 self.client.error_message.append(str(message))
564                 return
566             # user info
567             address = self.db.user.get(uid, 'address')
568             name = self.db.user.get(uid, 'username')
570             # send the email
571             tracker_name = self.db.config.TRACKER_NAME
572             subject = 'Password reset for %s'%tracker_name
573             body = '''
574 The password has been reset for username "%(name)s".
576 Your password is now: %(password)s
577 '''%{'name': name, 'password': newpw}
578             if not self.client.standard_message([address], subject, body):
579                 return
581             self.client.ok_message.append(
582                     'Password reset and email sent to %s'%address)
583             return
585         # no OTK, so now figure the user
586         if self.form.has_key('username'):
587             name = self.form['username'].value
588             try:
589                 uid = self.db.user.lookup(name)
590             except KeyError:
591                 self.client.error_message.append('Unknown username')
592                 return
593             address = self.db.user.get(uid, 'address')
594         elif self.form.has_key('address'):
595             address = self.form['address'].value
596             uid = uidFromAddress(self.db, ('', address), create=0)
597             if not uid:
598                 self.client.error_message.append('Unknown email address')
599                 return
600             name = self.db.user.get(uid, 'username')
601         else:
602             self.client.error_message.append('You need to specify a username '
603                 'or address')
604             return
606         # generate the one-time-key and store the props for later
607         otk = ''.join([random.choice(chars) for x in range(32)])
608         while otks.exists(otk):
609             otk = ''.join([random.choice(chars) for x in range(32)])
610         otks.set(otk, uid=uid)
611         self.db.commit()
613         # send the email
614         tracker_name = self.db.config.TRACKER_NAME
615         subject = 'Confirm reset of password for %s'%tracker_name
616         body = '''
617 Someone, perhaps you, has requested that the password be changed for your
618 username, "%(name)s". If you wish to proceed with the change, please follow
619 the link below:
621   %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
623 You should then receive another email with the new password.
624 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
625         if not self.client.standard_message([address], subject, body):
626             return
628         self.client.ok_message.append('Email sent to %s'%address)
630 class ConfRegoAction(Action):
631     def handle(self):
632         """Grab the OTK, use it to load up the new user details."""
633         try:
634             # pull the rego information out of the otk database
635             self.userid = self.db.confirm_registration(self.form['otk'].value)
636         except (ValueError, KeyError), message:
637             self.client.error_message.append(str(message))
638             return
640         # log the new user in
641         self.client.user = self.db.user.get(self.userid, 'username')
642         # re-open the database for real, using the user
643         self.client.opendb(self.client.user)
645         # if we have a session, update it
646         if hasattr(self, 'session'):
647             self.client.db.sessions.set(self.session, user=self.user,
648                 last_use=time.time())
649         else:
650             # new session cookie
651             self.client.set_cookie(self.user)
653         # nice message
654         message = _('You are now registered, welcome!')
655         url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
656             urllib.quote(message))
658         # redirect to the user's page (but not 302, as some email clients seem
659         # to want to reload the page, or something)
660         return '''<html><head><title>%s</title></head>
661             <body><p><a href="%s">%s</a></p>
662             <script type="text/javascript">
663             window.setTimeout('window.location = "%s"', 1000);
664             </script>'''%(message, url, message, url)
666 class RegisterAction(Action):
667     name = 'register'
668     permissionType = 'Web Registration'
670     def handle(self):
671         """Attempt to create a new user based on the contents of the form
672         and then set the cookie.
674         Return 1 on successful login.
675         """
676         props = self.client.parsePropsFromForm(create=1)[0][('user', None)]
678         # registration isn't allowed to supply roles
679         if props.has_key('roles'):
680             raise Unauthorised, _("It is not permitted to supply roles "
681                 "at registration.")
683         username = props['username']
684         try:
685             self.db.user.lookup(username)
686             self.client.error_message.append(_('Error: A user with the '
687                 'username "%(username)s" already exists')%props)
688             return
689         except KeyError:
690             pass
692         # generate the one-time-key and store the props for later
693         for propname, proptype in self.db.user.getprops().items():
694             value = props.get(propname, None)
695             if value is None:
696                 pass
697             elif isinstance(proptype, hyperdb.Date):
698                 props[propname] = str(value)
699             elif isinstance(proptype, hyperdb.Interval):
700                 props[propname] = str(value)
701             elif isinstance(proptype, hyperdb.Password):
702                 props[propname] = str(value)
703         otks = self.db.getOTKManager()
704         while otks.exists(otk):
705             otk = ''.join([random.choice(chars) for x in range(32)])
706         otks.set(otk, **props)
708         # send the email
709         tracker_name = self.db.config.TRACKER_NAME
710         tracker_email = self.db.config.TRACKER_EMAIL
711         subject = 'Complete your registration to %s -- key %s'%(tracker_name,
712                                                                   otk)
713         body = """To complete your registration of the user "%(name)s" with
714 %(tracker)s, please do one of the following:
716 - send a reply to %(tracker_email)s and maintain the subject line as is (the
717 reply's additional "Re:" is ok),
719 - or visit the following URL:
721 %(url)s?@action=confrego&otk=%(otk)s
723 """ % {'name': props['username'], 'tracker': tracker_name, 'url': self.base,
724         'otk': otk, 'tracker_email': tracker_email}
725         if not self.client.standard_message([props['address']], subject, body,
726         tracker_email):
727             return
729         # commit changes to the database
730         self.db.commit()
732         # redirect to the "you're almost there" page
733         raise Redirect, '%suser?@template=rego_progress'%self.base
735 class LogoutAction(Action):
736     def handle(self):
737         """Make us really anonymous - nuke the cookie too."""
738         # log us out
739         self.client.make_user_anonymous()
741         # construct the logout cookie
742         now = Cookie._getdate()
743         self.client.additional_headers['Set-Cookie'] = \
744            '%s=deleted; Max-Age=0; expires=%s; Path=%s;'%(self.client.cookie_name,
745             now, self.client.cookie_path)
747         # Let the user know what's going on
748         self.client.ok_message.append(_('You are logged out'))
750 class LoginAction(Action):
751     def handle(self):
752         """Attempt to log a user in.
754         Sets up a session for the user which contains the login credentials.
756         """
757         # we need the username at a minimum
758         if not self.form.has_key('__login_name'):
759             self.client.error_message.append(_('Username required'))
760             return
762         # get the login info
763         self.client.user = self.form['__login_name'].value
764         if self.form.has_key('__login_password'):
765             password = self.form['__login_password'].value
766         else:
767             password = ''
769         # make sure the user exists
770         try:
771             self.client.userid = self.db.user.lookup(self.client.user)
772         except KeyError:
773             name = self.client.user
774             self.client.error_message.append(_('No such user "%(name)s"')%locals())
775             self.client.make_user_anonymous()
776             return
778         # verify the password
779         if not self.verifyPassword(self.client.userid, password):
780             self.client.make_user_anonymous()
781             self.client.error_message.append(_('Incorrect password'))
782             return
784         # Determine whether the user has permission to log in.
785         # Base behaviour is to check the user has "Web Access".
786         if not self.hasPermission("Web Access"):
787             self.client.make_user_anonymous()
788             self.client.error_message.append(_("You do not have permission to login"))
789             return
791         # now we're OK, re-open the database for real, using the user
792         self.client.opendb(self.client.user)
794         # set the session cookie
795         self.client.set_cookie(self.client.user)
797     def verifyPassword(self, userid, password):
798         ''' Verify the password that the user has supplied
799         '''
800         stored = self.db.user.get(self.client.userid, 'password')
801         if password == stored:
802             return 1
803         if not password and not stored:
804             return 1
805         return 0
807 class ExportCSVAction(Action):
808     name = 'export'
809     permissionType = 'View'
811     def handle(self):
812         ''' Export the specified search query as CSV. '''
813         # figure the request
814         request = HTMLRequest(self)
815         filterspec = request.filterspec
816         sort = request.sort
817         group = request.group
818         columns = request.columns
819         klass = self.db.getclass(request.classname)
821         # full-text search
822         if request.search_text:
823             matches = self.db.indexer.search(
824                 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
825         else:
826             matches = None
828         h = self.additional_headers
829         h['Content-Type'] = 'text/csv'
830         # some browsers will honor the filename here...
831         h['Content-Disposition'] = 'inline; filename=query.csv'
832         self.header()
833         writer = rcsv.writer(self.request.wfile)
834         writer.writerow(columns)
836         # and search
837         for itemid in klass.filter(matches, filterspec, sort, group):
838             writer.writerow([str(klass.get(itemid, col)) for col in columns])
840         return '\n'
842 # vim: set filetype=python ts=4 sw=4 et si