6c00f0e6a9df135e87966a2239714c2562999b38
1 #$Id: actions.py,v 1.18 2004-03-26 06:31:50 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 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.lastUserActvity()
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 = HTMLRequest(self)
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.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.header()
855 writer = rcsv.writer(self.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