Code

fixed CSV export and CGI actions returning results
[roundup.git] / roundup / cgi / actions.py
1 #$Id: actions.py,v 1.21 2004-03-30 06:43:08 richard Exp $
3 import re, cgi, StringIO, urllib, Cookie, time, random
5 from roundup import hyperdb, token, date, password, rcsv, exceptions
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         return 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             key = self.db.query.getkey()
138             if key:
139                 # edit the old way, only one query per name
140                 try:
141                     qid = self.db.query.lookup(queryname)
142                     self.db.query.set(qid, klass=self.classname, url=url)
143                 except KeyError:
144                     # create a query
145                     qid = self.db.query.create(name=queryname,
146                         klass=self.classname, url=url)
147             else:
148                 # edit the new way, query name not a key any more
149                 # see if we match an existing private query
150                 uid = self.db.getuid()
151                 qids = self.db.query.filter({}, {'name': queryname,
152                         'private_for': uid})
153                 if not qids:
154                     # ok, so there's not a private query for the current user
155                     # - see if there's a public one created by them
156                     qids = self.db.query.filter({}, {'name': queryname,
157                         'private_for': -1, 'creator': uid})
159                 if qids:
160                     # edit query
161                     qid = qids[0]
162                     self.db.query.set(qid, klass=self.classname, url=url)
163                 else:
164                     # create a query
165                     qid = self.db.query.create(name=queryname,
166                         klass=self.classname, url=url, private_for=uid)
168             # and add it to the user's query multilink
169             queries = self.db.user.get(self.userid, 'queries')
170             if qid not in queries:
171                 queries.append(qid)
172                 self.db.user.set(self.userid, queries=queries)
174             # commit the query change to the database
175             self.db.commit()
177     def fakeFilterVars(self):
178         """Add a faked :filter form variable for each filtering prop."""
179         props = self.db.classes[self.classname].getprops()
180         for key in self.form.keys():
181             if not props.has_key(key):
182                 continue
183             if isinstance(self.form[key], type([])):
184                 # search for at least one entry which is not empty
185                 for minifield in self.form[key]:
186                     if minifield.value:
187                         break
188                 else:
189                     continue
190             else:
191                 if not self.form[key].value:
192                     continue
193                 if isinstance(props[key], hyperdb.String):
194                     v = self.form[key].value
195                     l = token.token_split(v)
196                     if len(l) > 1 or l[0] != v:
197                         self.form.value.remove(self.form[key])
198                         # replace the single value with the split list
199                         for v in l:
200                             self.form.value.append(cgi.MiniFieldStorage(key, v))
202             self.form.value.append(cgi.MiniFieldStorage('@filter', key))
204     FV_QUERYNAME = re.compile(r'[@:]queryname')
205     def getQueryName(self):
206         for key in self.form.keys():
207             if self.FV_QUERYNAME.match(key):
208                 return self.form[key].value.strip()
209         return ''
211 class EditCSVAction(Action):
212     name = 'edit'
213     permissionType = 'Edit'
215     def handle(self):
216         """Performs an edit of all of a class' items in one go.
218         The "rows" CGI var defines the CSV-formatted entries for the class. New
219         nodes are identified by the ID 'X' (or any other non-existent ID) and
220         removed lines are retired.
222         """
223         # get the CSV module
224         if rcsv.error:
225             self.client.error_message.append(_(rcsv.error))
226             return
228         cl = self.db.classes[self.classname]
229         idlessprops = cl.getprops(protected=0).keys()
230         idlessprops.sort()
231         props = ['id'] + idlessprops
233         # do the edit
234         rows = StringIO.StringIO(self.form['rows'].value)
235         reader = rcsv.reader(rows, rcsv.comma_separated)
236         found = {}
237         line = 0
238         for values in reader:
239             line += 1
240             if line == 1: continue
241             # skip property names header
242             if values == props:
243                 continue
245             # extract the nodeid
246             nodeid, values = values[0], values[1:]
247             found[nodeid] = 1
249             # see if the node exists
250             if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
251                 exists = 0
252             else:
253                 exists = 1
255             # confirm correct weight
256             if len(idlessprops) != len(values):
257                 self.client.error_message.append(
258                     _('Not enough values on line %(line)s')%{'line':line})
259                 return
261             # extract the new values
262             d = {}
263             for name, value in zip(idlessprops, values):
264                 prop = cl.properties[name]
265                 value = value.strip()
266                 # only add the property if it has a value
267                 if value:
268                     # if it's a multilink, split it
269                     if isinstance(prop, hyperdb.Multilink):
270                         value = value.split(':')
271                     elif isinstance(prop, hyperdb.Password):
272                         value = password.Password(value)
273                     elif isinstance(prop, hyperdb.Interval):
274                         value = date.Interval(value)
275                     elif isinstance(prop, hyperdb.Date):
276                         value = date.Date(value)
277                     elif isinstance(prop, hyperdb.Boolean):
278                         value = value.lower() in ('yes', 'true', 'on', '1')
279                     elif isinstance(prop, hyperdb.Number):
280                         value = float(value)
281                     d[name] = value
282                 elif exists:
283                     # nuke the existing value
284                     if isinstance(prop, hyperdb.Multilink):
285                         d[name] = []
286                     else:
287                         d[name] = None
289             # perform the edit
290             if exists:
291                 # edit existing
292                 cl.set(nodeid, **d)
293             else:
294                 # new node
295                 found[cl.create(**d)] = 1
297         # retire the removed entries
298         for nodeid in cl.list():
299             if not found.has_key(nodeid):
300                 cl.retire(nodeid)
302         # all OK
303         self.db.commit()
305         self.client.ok_message.append(_('Items edited OK'))
307 class _EditAction(Action):
308     def isEditingSelf(self):
309         """Check whether a user is editing his/her own details."""
310         return (self.nodeid == self.userid
311                 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
313     def editItemPermission(self, props):
314         """Determine whether the user has permission to edit this item.
316         Base behaviour is to check the user can edit this class. If we're
317         editing the "user" class, users are allowed to edit their own details.
318         Unless it's the "roles" property, which requires the special Permission
319         "Web Roles".
320         """
321         if self.classname == 'user':
322             if props.has_key('roles') and not self.hasPermission('Web Roles'):
323                 raise Unauthorised, _("You do not have permission to edit user roles")
324             if self.isEditingSelf():
325                 return 1
326         if self.hasPermission('Edit'):
327             return 1
328         return 0
330     def newItemPermission(self, props):
331         """Determine whether the user has permission to create (edit) this item.
333         Base behaviour is to check the user can edit this class. No additional
334         property checks are made. Additionally, new user items may be created
335         if the user has the "Web Registration" Permission.
337         """
338         if (self.classname == 'user' and self.hasPermission('Web Registration')
339             or self.hasPermission('Edit')):
340             return 1
341         return 0
343     #
344     #  Utility methods for editing
345     #
346     def _editnodes(self, all_props, all_links, newids=None):
347         ''' Use the props in all_props to perform edit and creation, then
348             use the link specs in all_links to do linking.
349         '''
350         # figure dependencies and re-work links
351         deps = {}
352         links = {}
353         for cn, nodeid, propname, vlist in all_links:
354             if not all_props.has_key((cn, nodeid)):
355                 # link item to link to doesn't (and won't) exist
356                 continue
357             for value in vlist:
358                 if not all_props.has_key(value):
359                     # link item to link to doesn't (and won't) exist
360                     continue
361                 deps.setdefault((cn, nodeid), []).append(value)
362                 links.setdefault(value, []).append((cn, nodeid, propname))
364         # figure chained dependencies ordering
365         order = []
366         done = {}
367         # loop detection
368         change = 0
369         while len(all_props) != len(done):
370             for needed in all_props.keys():
371                 if done.has_key(needed):
372                     continue
373                 tlist = deps.get(needed, [])
374                 for target in tlist:
375                     if not done.has_key(target):
376                         break
377                 else:
378                     done[needed] = 1
379                     order.append(needed)
380                     change = 1
381             if not change:
382                 raise ValueError, 'linking must not loop!'
384         # now, edit / create
385         m = []
386         for needed in order:
387             props = all_props[needed]
388             if not props:
389                 # nothing to do
390                 continue
391             cn, nodeid = needed
393             if nodeid is not None and int(nodeid) > 0:
394                 # make changes to the node
395                 props = self._changenode(cn, nodeid, props)
397                 # and some nice feedback for the user
398                 if props:
399                     info = ', '.join(props.keys())
400                     m.append('%s %s %s edited ok'%(cn, nodeid, info))
401                 else:
402                     m.append('%s %s - nothing changed'%(cn, nodeid))
403             else:
404                 assert props
406                 # make a new node
407                 newid = self._createnode(cn, props)
408                 if nodeid is None:
409                     self.nodeid = newid
410                 nodeid = newid
412                 # and some nice feedback for the user
413                 m.append('%s %s created'%(cn, newid))
415             # fill in new ids in links
416             if links.has_key(needed):
417                 for linkcn, linkid, linkprop in links[needed]:
418                     props = all_props[(linkcn, linkid)]
419                     cl = self.db.classes[linkcn]
420                     propdef = cl.getprops()[linkprop]
421                     if not props.has_key(linkprop):
422                         if linkid is None or linkid.startswith('-'):
423                             # linking to a new item
424                             if isinstance(propdef, hyperdb.Multilink):
425                                 props[linkprop] = [newid]
426                             else:
427                                 props[linkprop] = newid
428                         else:
429                             # linking to an existing item
430                             if isinstance(propdef, hyperdb.Multilink):
431                                 existing = cl.get(linkid, linkprop)[:]
432                                 existing.append(nodeid)
433                                 props[linkprop] = existing
434                             else:
435                                 props[linkprop] = newid
437         return '<br>'.join(m)
439     def _changenode(self, cn, nodeid, props):
440         """Change the node based on the contents of the form."""
441         # check for permission
442         if not self.editItemPermission(props):
443             raise Unauthorised, 'You do not have permission to edit %s'%cn
445         # make the changes
446         cl = self.db.classes[cn]
447         return cl.set(nodeid, **props)
449     def _createnode(self, cn, props):
450         """Create a node based on the contents of the form."""
451         # check for permission
452         if not self.newItemPermission(props):
453             raise Unauthorised, 'You do not have permission to create %s'%cn
455         # create the node and return its id
456         cl = self.db.classes[cn]
457         return cl.create(**props)
459 class EditItemAction(_EditAction):
460     def lastUserActivity(self):
461         if self.form.has_key(':lastactivity'):
462             return date.Date(self.form[':lastactivity'].value)
463         elif self.form.has_key('@lastactivity'):
464             return date.Date(self.form['@lastactivity'].value)
465         else:
466             return None
468     def lastNodeActivity(self):
469         cl = getattr(self.client.db, self.classname)
470         return cl.get(self.nodeid, 'activity')
472     def detectCollision(self, user_activity, node_activity):
473         if user_activity:
474             return user_activity < node_activity
476     def handleCollision(self):
477         self.client.template = 'collision'
479     def handle(self):
480         """Perform an edit of an item in the database.
482         See parsePropsFromForm and _editnodes for special variables.
484         """
485         user_activity = self.lastUserActivity()
486         if user_activity and self.detectCollision(user_activity,
487                 self.lastNodeActivity()):
488             self.handleCollision()
489             return
491         props, links = self.client.parsePropsFromForm()
493         # handle the props
494         try:
495             message = self._editnodes(props, links)
496         except (ValueError, KeyError, IndexError, exceptions.Reject), message:
497             self.client.error_message.append(_('Apply Error: ') + str(message))
498             return
500         # commit now that all the tricky stuff is done
501         self.db.commit()
503         # redirect to the item's edit page
504         # redirect to finish off
505         url = self.base + self.classname
506         # note that this action might have been called by an index page, so
507         # we will want to include index-page args in this URL too
508         if self.nodeid is not None:
509             url += self.nodeid
510         url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
511             urllib.quote(self.template))
512         if self.nodeid is None:
513             req = templating.HTMLRequest(self.client)
514             url += '&' + req.indexargs_href('', {})[1:]
515         raise Redirect, url
517 class NewItemAction(_EditAction):
518     def handle(self):
519         ''' Add a new item to the database.
521             This follows the same form as the EditItemAction, with the same
522             special form values.
523         '''
524         # parse the props from the form
525         try:
526             props, links = self.client.parsePropsFromForm(create=1)
527         except (ValueError, KeyError), message:
528             self.client.error_message.append(_('Error: ') + str(message))
529             return
531         # handle the props - edit or create
532         try:
533             # when it hits the None element, it'll set self.nodeid
534             messages = self._editnodes(props, links)
536         except (ValueError, KeyError, IndexError, exceptions.Reject), message:
537             # these errors might just be indicative of user dumbness
538             self.client.error_message.append(_('Error: ') + str(message))
539             return
541         # commit now that all the tricky stuff is done
542         self.db.commit()
544         # redirect to the new item's page
545         raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
546             self.classname, self.nodeid, urllib.quote(messages),
547             urllib.quote(self.template))
549 class PassResetAction(Action):
550     def handle(self):
551         """Handle password reset requests.
553         Presence of either "name" or "address" generates email. Presence of
554         "otk" performs the reset.
556         """
557         if self.form.has_key('otk'):
558             # pull the rego information out of the otk database
559             otk = self.form['otk'].value
560             otks = self.db.getOTKManager()
561             uid = otks.get(otk, 'uid')
562             if uid is None:
563                 self.client.error_message.append("""Invalid One Time Key!
564 (a Mozilla bug may cause this message to show up erroneously,
565  please check your email)""")
566                 return
568             # re-open the database as "admin"
569             if self.user != 'admin':
570                 self.client.opendb('admin')
571                 self.db = self.client.db
573             # change the password
574             newpw = password.generatePassword()
576             cl = self.db.user
577             # XXX we need to make the "default" page be able to display errors!
578             try:
579                 # set the password
580                 cl.set(uid, password=password.Password(newpw))
581                 # clear the props from the otk database
582                 otks.destroy(otk)
583                 self.db.commit()
584             except (ValueError, KeyError), message:
585                 self.client.error_message.append(str(message))
586                 return
588             # user info
589             address = self.db.user.get(uid, 'address')
590             name = self.db.user.get(uid, 'username')
592             # send the email
593             tracker_name = self.db.config.TRACKER_NAME
594             subject = 'Password reset for %s'%tracker_name
595             body = '''
596 The password has been reset for username "%(name)s".
598 Your password is now: %(password)s
599 '''%{'name': name, 'password': newpw}
600             if not self.client.standard_message([address], subject, body):
601                 return
603             self.client.ok_message.append(
604                     'Password reset and email sent to %s'%address)
605             return
607         # no OTK, so now figure the user
608         if self.form.has_key('username'):
609             name = self.form['username'].value
610             try:
611                 uid = self.db.user.lookup(name)
612             except KeyError:
613                 self.client.error_message.append('Unknown username')
614                 return
615             address = self.db.user.get(uid, 'address')
616         elif self.form.has_key('address'):
617             address = self.form['address'].value
618             uid = uidFromAddress(self.db, ('', address), create=0)
619             if not uid:
620                 self.client.error_message.append('Unknown email address')
621                 return
622             name = self.db.user.get(uid, 'username')
623         else:
624             self.client.error_message.append('You need to specify a username '
625                 'or address')
626             return
628         # generate the one-time-key and store the props for later
629         otk = ''.join([random.choice(chars) for x in range(32)])
630         while otks.exists(otk):
631             otk = ''.join([random.choice(chars) for x in range(32)])
632         otks.set(otk, uid=uid)
633         self.db.commit()
635         # send the email
636         tracker_name = self.db.config.TRACKER_NAME
637         subject = 'Confirm reset of password for %s'%tracker_name
638         body = '''
639 Someone, perhaps you, has requested that the password be changed for your
640 username, "%(name)s". If you wish to proceed with the change, please follow
641 the link below:
643   %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
645 You should then receive another email with the new password.
646 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
647         if not self.client.standard_message([address], subject, body):
648             return
650         self.client.ok_message.append('Email sent to %s'%address)
652 class ConfRegoAction(Action):
653     def handle(self):
654         """Grab the OTK, use it to load up the new user details."""
655         try:
656             # pull the rego information out of the otk database
657             self.userid = self.db.confirm_registration(self.form['otk'].value)
658         except (ValueError, KeyError), message:
659             self.client.error_message.append(str(message))
660             return
662         # log the new user in
663         self.client.user = self.db.user.get(self.userid, 'username')
664         # re-open the database for real, using the user
665         self.client.opendb(self.client.user)
667         # if we have a session, update it
668         if hasattr(self, 'session'):
669             self.client.db.sessions.set(self.session, user=self.user,
670                 last_use=time.time())
671         else:
672             # new session cookie
673             self.client.set_cookie(self.user)
675         # nice message
676         message = _('You are now registered, welcome!')
677         url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
678             urllib.quote(message))
680         # redirect to the user's page (but not 302, as some email clients seem
681         # to want to reload the page, or something)
682         return '''<html><head><title>%s</title></head>
683             <body><p><a href="%s">%s</a></p>
684             <script type="text/javascript">
685             window.setTimeout('window.location = "%s"', 1000);
686             </script>'''%(message, url, message, url)
688 class RegisterAction(Action):
689     name = 'register'
690     permissionType = 'Web Registration'
692     def handle(self):
693         """Attempt to create a new user based on the contents of the form
694         and then set the cookie.
696         Return 1 on successful login.
697         """
698         props = self.client.parsePropsFromForm(create=1)[0][('user', None)]
700         # registration isn't allowed to supply roles
701         if props.has_key('roles'):
702             raise Unauthorised, _("It is not permitted to supply roles "
703                 "at registration.")
705         username = props['username']
706         try:
707             self.db.user.lookup(username)
708             self.client.error_message.append(_('Error: A user with the '
709                 'username "%(username)s" already exists')%props)
710             return
711         except KeyError:
712             pass
714         # generate the one-time-key and store the props for later
715         for propname, proptype in self.db.user.getprops().items():
716             value = props.get(propname, None)
717             if value is None:
718                 pass
719             elif isinstance(proptype, hyperdb.Date):
720                 props[propname] = str(value)
721             elif isinstance(proptype, hyperdb.Interval):
722                 props[propname] = str(value)
723             elif isinstance(proptype, hyperdb.Password):
724                 props[propname] = str(value)
725         otks = self.db.getOTKManager()
726         while otks.exists(otk):
727             otk = ''.join([random.choice(chars) for x in range(32)])
728         otks.set(otk, **props)
730         # send the email
731         tracker_name = self.db.config.TRACKER_NAME
732         tracker_email = self.db.config.TRACKER_EMAIL
733         subject = 'Complete your registration to %s -- key %s'%(tracker_name,
734                                                                   otk)
735         body = """To complete your registration of the user "%(name)s" with
736 %(tracker)s, please do one of the following:
738 - send a reply to %(tracker_email)s and maintain the subject line as is (the
739 reply's additional "Re:" is ok),
741 - or visit the following URL:
743 %(url)s?@action=confrego&otk=%(otk)s
745 """ % {'name': props['username'], 'tracker': tracker_name, 'url': self.base,
746         'otk': otk, 'tracker_email': tracker_email}
747         if not self.client.standard_message([props['address']], subject, body,
748         tracker_email):
749             return
751         # commit changes to the database
752         self.db.commit()
754         # redirect to the "you're almost there" page
755         raise Redirect, '%suser?@template=rego_progress'%self.base
757 class LogoutAction(Action):
758     def handle(self):
759         """Make us really anonymous - nuke the cookie too."""
760         # log us out
761         self.client.make_user_anonymous()
763         # construct the logout cookie
764         now = Cookie._getdate()
765         self.client.additional_headers['Set-Cookie'] = \
766            '%s=deleted; Max-Age=0; expires=%s; Path=%s;'%(self.client.cookie_name,
767             now, self.client.cookie_path)
769         # Let the user know what's going on
770         self.client.ok_message.append(_('You are logged out'))
772 class LoginAction(Action):
773     def handle(self):
774         """Attempt to log a user in.
776         Sets up a session for the user which contains the login credentials.
778         """
779         # we need the username at a minimum
780         if not self.form.has_key('__login_name'):
781             self.client.error_message.append(_('Username required'))
782             return
784         # get the login info
785         self.client.user = self.form['__login_name'].value
786         if self.form.has_key('__login_password'):
787             password = self.form['__login_password'].value
788         else:
789             password = ''
791         # make sure the user exists
792         try:
793             self.client.userid = self.db.user.lookup(self.client.user)
794         except KeyError:
795             name = self.client.user
796             self.client.error_message.append(_('No such user "%(name)s"')%locals())
797             self.client.make_user_anonymous()
798             return
800         # verify the password
801         if not self.verifyPassword(self.client.userid, password):
802             self.client.make_user_anonymous()
803             self.client.error_message.append(_('Incorrect password'))
804             return
806         # Determine whether the user has permission to log in.
807         # Base behaviour is to check the user has "Web Access".
808         if not self.hasPermission("Web Access"):
809             self.client.make_user_anonymous()
810             self.client.error_message.append(_("You do not have permission to login"))
811             return
813         # now we're OK, re-open the database for real, using the user
814         self.client.opendb(self.client.user)
816         # set the session cookie
817         self.client.set_cookie(self.client.user)
819     def verifyPassword(self, userid, password):
820         ''' Verify the password that the user has supplied
821         '''
822         stored = self.db.user.get(self.client.userid, 'password')
823         if password == stored:
824             return 1
825         if not password and not stored:
826             return 1
827         return 0
829 class ExportCSVAction(Action):
830     name = 'export'
831     permissionType = 'View'
833     def handle(self):
834         ''' Export the specified search query as CSV. '''
835         # figure the request
836         request = templating.HTMLRequest(self.client)
837         filterspec = request.filterspec
838         sort = request.sort
839         group = request.group
840         columns = request.columns
841         klass = self.db.getclass(request.classname)
843         # full-text search
844         if request.search_text:
845             matches = self.db.indexer.search(
846                 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
847         else:
848             matches = None
850         h = self.client.additional_headers
851         h['Content-Type'] = 'text/csv'
852         # some browsers will honor the filename here...
853         h['Content-Disposition'] = 'inline; filename=query.csv'
854         self.client.header()
855         writer = rcsv.writer(self.client.request.wfile)
856         writer.writerow(columns)
858         # and search
859         for itemid in klass.filter(matches, filterspec, sort, group):
860             writer.writerow([str(klass.get(itemid, col)) for col in columns])
862         return '\n'
864 # vim: set filetype=python ts=4 sw=4 et si