ac43e4c0a91fe1164b6e1379bf01c695607738db
1 #$Id: actions.py,v 1.73 2008-08-18 05:04:01 richard Exp $
3 import re, cgi, StringIO, urllib, time, random, csv, codecs
5 from roundup import hyperdb, token, date, password
6 from roundup.i18n import _
7 import roundup.exceptions
8 from roundup.cgi import exceptions, templating
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
30 self.context = templating.context(client)
32 def handle(self):
33 """Action handler procedure"""
34 raise NotImplementedError
36 def execute(self):
37 """Execute the action specified by this object."""
38 self.permission()
39 return self.handle()
41 name = ''
42 permissionType = None
43 def permission(self):
44 """Check whether the user has permission to execute this action.
46 True by default. If the permissionType attribute is a string containing
47 a simple permission, check whether the user has that permission.
48 Subclasses must also define the name attribute if they define
49 permissionType.
51 Despite having this permission, users may still be unauthorised to
52 perform parts of actions. It is up to the subclasses to detect this.
53 """
54 if (self.permissionType and
55 not self.hasPermission(self.permissionType)):
56 info = {'action': self.name, 'classname': self.classname}
57 raise exceptions.Unauthorised, self._(
58 'You do not have permission to '
59 '%(action)s the %(classname)s class.')%info
61 _marker = []
62 def hasPermission(self, permission, classname=_marker, itemid=None):
63 """Check whether the user has 'permission' on the current class."""
64 if classname is self._marker:
65 classname = self.client.classname
66 return self.db.security.hasPermission(permission, self.client.userid,
67 classname=classname, itemid=itemid)
69 def gettext(self, msgid):
70 """Return the localized translation of msgid"""
71 return self.client.translator.gettext(msgid)
73 _ = gettext
75 class ShowAction(Action):
77 typere=re.compile('[@:]type')
78 numre=re.compile('[@:]number')
80 def handle(self):
81 """Show a node of a particular class/id."""
82 t = n = ''
83 for key in self.form.keys():
84 if self.typere.match(key):
85 t = self.form[key].value.strip()
86 elif self.numre.match(key):
87 n = self.form[key].value.strip()
88 if not t:
89 raise ValueError, self._('No type specified')
90 if not n:
91 raise exceptions.SeriousError, self._('No ID entered')
92 try:
93 int(n)
94 except ValueError:
95 d = {'input': n, 'classname': t}
96 raise exceptions.SeriousError, self._(
97 '"%(input)s" is not an ID (%(classname)s ID required)')%d
98 url = '%s%s%s'%(self.base, t, n)
99 raise exceptions.Redirect, url
101 class RetireAction(Action):
102 name = 'retire'
103 permissionType = 'Edit'
105 def handle(self):
106 """Retire the context item."""
107 # if we want to view the index template now, then unset the nodeid
108 # context info (a special-case for retire actions on the index page)
109 nodeid = self.nodeid
110 if self.template == 'index':
111 self.client.nodeid = None
113 # make sure we don't try to retire admin or anonymous
114 if self.classname == 'user' and \
115 self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
116 raise ValueError, self._(
117 'You may not retire the admin or anonymous user')
119 # do the retire
120 self.db.getclass(self.classname).retire(nodeid)
121 self.db.commit()
123 self.client.ok_message.append(
124 self._('%(classname)s %(itemid)s has been retired')%{
125 'classname': self.classname.capitalize(), 'itemid': nodeid})
127 def hasPermission(self, permission, classname=Action._marker, itemid=None):
128 if itemid is None:
129 itemid = self.nodeid
130 return Action.hasPermission(self, permission, classname, itemid)
132 class SearchAction(Action):
133 name = 'search'
134 permissionType = 'View'
136 def handle(self):
137 """Mangle some of the form variables.
139 Set the form ":filter" variable based on the values of the filter
140 variables - if they're set to anything other than "dontcare" then add
141 them to :filter.
143 Handle the ":queryname" variable and save off the query to the user's
144 query list.
146 Split any String query values on whitespace and comma.
148 """
149 self.fakeFilterVars()
150 queryname = self.getQueryName()
152 # editing existing query name?
153 old_queryname = self.getFromForm('old-queryname')
155 # handle saving the query params
156 if queryname:
157 # parse the environment and figure what the query _is_
158 req = templating.HTMLRequest(self.client)
160 url = self.getCurrentURL(req)
162 key = self.db.query.getkey()
163 if key:
164 # edit the old way, only one query per name
165 try:
166 qid = self.db.query.lookup(old_queryname)
167 if not self.hasPermission('Edit', 'query', itemid=qid):
168 raise exceptions.Unauthorised, self._(
169 "You do not have permission to edit queries")
170 self.db.query.set(qid, klass=self.classname, url=url)
171 except KeyError:
172 # create a query
173 if not self.hasPermission('Create', 'query'):
174 raise exceptions.Unauthorised, self._(
175 "You do not have permission to store queries")
176 qid = self.db.query.create(name=queryname,
177 klass=self.classname, url=url)
178 else:
179 # edit the new way, query name not a key any more
180 # see if we match an existing private query
181 uid = self.db.getuid()
182 qids = self.db.query.filter(None, {'name': old_queryname,
183 'private_for': uid})
184 if not qids:
185 # ok, so there's not a private query for the current user
186 # - see if there's one created by them
187 qids = self.db.query.filter(None, {'name': old_queryname,
188 'creator': uid})
190 if qids and old_queryname:
191 # edit query - make sure we get an exact match on the name
192 for qid in qids:
193 if old_queryname != self.db.query.get(qid, 'name'):
194 continue
195 if not self.hasPermission('Edit', 'query', itemid=qid):
196 raise exceptions.Unauthorised, self._(
197 "You do not have permission to edit queries")
198 self.db.query.set(qid, klass=self.classname,
199 url=url, name=queryname)
200 else:
201 # create a query
202 if not self.hasPermission('Create', 'query'):
203 raise exceptions.Unauthorised, self._(
204 "You do not have permission to store queries")
205 qid = self.db.query.create(name=queryname,
206 klass=self.classname, url=url, private_for=uid)
208 # and add it to the user's query multilink
209 queries = self.db.user.get(self.userid, 'queries')
210 if qid not in queries:
211 queries.append(qid)
212 self.db.user.set(self.userid, queries=queries)
214 # commit the query change to the database
215 self.db.commit()
217 def fakeFilterVars(self):
218 """Add a faked :filter form variable for each filtering prop."""
219 cls = self.db.classes[self.classname]
220 for key in self.form.keys():
221 prop = cls.get_transitive_prop(key)
222 if not prop:
223 continue
224 if isinstance(self.form[key], type([])):
225 # search for at least one entry which is not empty
226 for minifield in self.form[key]:
227 if minifield.value:
228 break
229 else:
230 continue
231 else:
232 if not self.form[key].value:
233 continue
234 if isinstance(prop, hyperdb.String):
235 v = self.form[key].value
236 l = token.token_split(v)
237 if len(l) > 1 or l[0] != v:
238 self.form.value.remove(self.form[key])
239 # replace the single value with the split list
240 for v in l:
241 self.form.value.append(cgi.MiniFieldStorage(key, v))
243 self.form.value.append(cgi.MiniFieldStorage('@filter', key))
245 def getCurrentURL(self, req):
246 """Get current URL for storing as a query.
248 Note: We are removing the first character from the current URL,
249 because the leading '?' is not part of the query string.
251 Implementation note:
252 But maybe the template should be part of the stored query:
253 template = self.getFromForm('template')
254 if template:
255 return req.indexargs_url('', {'@template' : template})[1:]
256 """
257 return req.indexargs_url('', {})[1:]
259 def getFromForm(self, name):
260 for key in ('@' + name, ':' + name):
261 if self.form.has_key(key):
262 return self.form[key].value.strip()
263 return ''
265 def getQueryName(self):
266 return self.getFromForm('queryname')
268 class EditCSVAction(Action):
269 name = 'edit'
270 permissionType = 'Edit'
272 def handle(self):
273 """Performs an edit of all of a class' items in one go.
275 The "rows" CGI var defines the CSV-formatted entries for the class. New
276 nodes are identified by the ID 'X' (or any other non-existent ID) and
277 removed lines are retired.
279 """
280 cl = self.db.classes[self.classname]
281 idlessprops = cl.getprops(protected=0).keys()
282 idlessprops.sort()
283 props = ['id'] + idlessprops
285 # do the edit
286 rows = StringIO.StringIO(self.form['rows'].value)
287 reader = csv.reader(rows)
288 found = {}
289 line = 0
290 for values in reader:
291 line += 1
292 if line == 1: continue
293 # skip property names header
294 if values == props:
295 continue
297 # extract the nodeid
298 nodeid, values = values[0], values[1:]
299 found[nodeid] = 1
301 # see if the node exists
302 if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
303 exists = 0
304 else:
305 exists = 1
307 # confirm correct weight
308 if len(idlessprops) != len(values):
309 self.client.error_message.append(
310 self._('Not enough values on line %(line)s')%{'line':line})
311 return
313 # extract the new values
314 d = {}
315 for name, value in zip(idlessprops, values):
316 prop = cl.properties[name]
317 value = value.strip()
318 # only add the property if it has a value
319 if value:
320 # if it's a multilink, split it
321 if isinstance(prop, hyperdb.Multilink):
322 value = value.split(':')
323 elif isinstance(prop, hyperdb.Password):
324 value = password.Password(value)
325 elif isinstance(prop, hyperdb.Interval):
326 value = date.Interval(value)
327 elif isinstance(prop, hyperdb.Date):
328 value = date.Date(value)
329 elif isinstance(prop, hyperdb.Boolean):
330 value = value.lower() in ('yes', 'true', 'on', '1')
331 elif isinstance(prop, hyperdb.Number):
332 value = float(value)
333 d[name] = value
334 elif exists:
335 # nuke the existing value
336 if isinstance(prop, hyperdb.Multilink):
337 d[name] = []
338 else:
339 d[name] = None
341 # perform the edit
342 if exists:
343 # edit existing
344 cl.set(nodeid, **d)
345 else:
346 # new node
347 found[cl.create(**d)] = 1
349 # retire the removed entries
350 for nodeid in cl.list():
351 if not found.has_key(nodeid):
352 cl.retire(nodeid)
354 # all OK
355 self.db.commit()
357 self.client.ok_message.append(self._('Items edited OK'))
359 class EditCommon(Action):
360 '''Utility methods for editing.'''
362 def _editnodes(self, all_props, all_links):
363 ''' Use the props in all_props to perform edit and creation, then
364 use the link specs in all_links to do linking.
365 '''
366 # figure dependencies and re-work links
367 deps = {}
368 links = {}
369 for cn, nodeid, propname, vlist in all_links:
370 numeric_id = int (nodeid or 0)
371 if not (numeric_id > 0 or all_props.has_key((cn, nodeid))):
372 # link item to link to doesn't (and won't) exist
373 continue
375 for value in vlist:
376 if not all_props.has_key(value):
377 # link item to link to doesn't (and won't) exist
378 continue
379 deps.setdefault((cn, nodeid), []).append(value)
380 links.setdefault(value, []).append((cn, nodeid, propname))
382 # figure chained dependencies ordering
383 order = []
384 done = {}
385 # loop detection
386 change = 0
387 while len(all_props) != len(done):
388 for needed in all_props.keys():
389 if done.has_key(needed):
390 continue
391 tlist = deps.get(needed, [])
392 for target in tlist:
393 if not done.has_key(target):
394 break
395 else:
396 done[needed] = 1
397 order.append(needed)
398 change = 1
399 if not change:
400 raise ValueError, 'linking must not loop!'
402 # now, edit / create
403 m = []
404 for needed in order:
405 props = all_props[needed]
406 cn, nodeid = needed
407 if props:
408 if nodeid is not None and int(nodeid) > 0:
409 # make changes to the node
410 props = self._changenode(cn, nodeid, props)
412 # and some nice feedback for the user
413 if props:
414 info = ', '.join(map(self._, props.keys()))
415 m.append(
416 self._('%(class)s %(id)s %(properties)s edited ok')
417 % {'class':cn, 'id':nodeid, 'properties':info})
418 else:
419 m.append(self._('%(class)s %(id)s - nothing changed')
420 % {'class':cn, 'id':nodeid})
421 else:
422 assert props
424 # make a new node
425 newid = self._createnode(cn, props)
426 if nodeid is None:
427 self.nodeid = newid
428 nodeid = newid
430 # and some nice feedback for the user
431 m.append(self._('%(class)s %(id)s created')
432 % {'class':cn, 'id':newid})
434 # fill in new ids in links
435 if links.has_key(needed):
436 for linkcn, linkid, linkprop in links[needed]:
437 props = all_props[(linkcn, linkid)]
438 cl = self.db.classes[linkcn]
439 propdef = cl.getprops()[linkprop]
440 if not props.has_key(linkprop):
441 if linkid is None or linkid.startswith('-'):
442 # linking to a new item
443 if isinstance(propdef, hyperdb.Multilink):
444 props[linkprop] = [newid]
445 else:
446 props[linkprop] = newid
447 else:
448 # linking to an existing item
449 if isinstance(propdef, hyperdb.Multilink):
450 existing = cl.get(linkid, linkprop)[:]
451 existing.append(nodeid)
452 props[linkprop] = existing
453 else:
454 props[linkprop] = newid
456 return '<br>'.join(m)
458 def _changenode(self, cn, nodeid, props):
459 """Change the node based on the contents of the form."""
460 # check for permission
461 if not self.editItemPermission(props, classname=cn, itemid=nodeid):
462 raise exceptions.Unauthorised, self._(
463 'You do not have permission to edit %(class)s'
464 ) % {'class': cn}
466 # make the changes
467 cl = self.db.classes[cn]
468 return cl.set(nodeid, **props)
470 def _createnode(self, cn, props):
471 """Create a node based on the contents of the form."""
472 # check for permission
473 if not self.newItemPermission(props, classname=cn):
474 raise exceptions.Unauthorised, self._(
475 'You do not have permission to create %(class)s'
476 ) % {'class': cn}
478 # create the node and return its id
479 cl = self.db.classes[cn]
480 return cl.create(**props)
482 def isEditingSelf(self):
483 """Check whether a user is editing his/her own details."""
484 return (self.nodeid == self.userid
485 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
487 _cn_marker = []
488 def editItemPermission(self, props, classname=_cn_marker, itemid=None):
489 """Determine whether the user has permission to edit this item.
491 Base behaviour is to check the user can edit this class. If we're
492 editing the "user" class, users are allowed to edit their own details.
493 Unless it's the "roles" property, which requires the special Permission
494 "Web Roles".
495 """
496 if self.classname == 'user':
497 if props.has_key('roles') and not self.hasPermission('Web Roles'):
498 raise exceptions.Unauthorised, self._(
499 "You do not have permission to edit user roles")
500 if self.isEditingSelf():
501 return 1
502 if itemid is None:
503 itemid = self.nodeid
504 if classname is self._cn_marker:
505 classname = self.classname
506 if self.hasPermission('Edit', itemid=itemid, classname=classname):
507 return 1
508 return 0
510 def newItemPermission(self, props, classname=None):
511 """Determine whether the user has permission to create this item.
513 Base behaviour is to check the user can edit this class. No additional
514 property checks are made.
515 """
516 if not classname :
517 classname = self.client.classname
518 return self.hasPermission('Create', classname=classname)
520 class EditItemAction(EditCommon):
521 def lastUserActivity(self):
522 if self.form.has_key(':lastactivity'):
523 d = date.Date(self.form[':lastactivity'].value)
524 elif self.form.has_key('@lastactivity'):
525 d = date.Date(self.form['@lastactivity'].value)
526 else:
527 return None
528 d.second = int(d.second)
529 return d
531 def lastNodeActivity(self):
532 cl = getattr(self.client.db, self.classname)
533 activity = cl.get(self.nodeid, 'activity').local(0)
534 activity.second = int(activity.second)
535 return activity
537 def detectCollision(self, user_activity, node_activity):
538 '''Check for a collision and return the list of props we edited
539 that conflict.'''
540 if user_activity and user_activity < node_activity:
541 props, links = self.client.parsePropsFromForm()
542 key = (self.classname, self.nodeid)
543 # we really only collide for direct prop edit conflicts
544 return props[key].keys()
545 else:
546 return []
548 def handleCollision(self, props):
549 message = self._('Edit Error: someone else has edited this %s (%s). '
550 'View <a target="new" href="%s%s">their changes</a> '
551 'in a new window.')%(self.classname, ', '.join(props),
552 self.classname, self.nodeid)
553 self.client.error_message.append(message)
554 return
556 def handle(self):
557 """Perform an edit of an item in the database.
559 See parsePropsFromForm and _editnodes for special variables.
561 """
562 user_activity = self.lastUserActivity()
563 if user_activity:
564 props = self.detectCollision(user_activity, self.lastNodeActivity())
565 if props:
566 self.handleCollision(props)
567 return
569 props, links = self.client.parsePropsFromForm()
571 # handle the props
572 try:
573 message = self._editnodes(props, links)
574 except (ValueError, KeyError, IndexError,
575 roundup.exceptions.Reject), message:
576 self.client.error_message.append(
577 self._('Edit Error: %s') % str(message))
578 return
580 # commit now that all the tricky stuff is done
581 self.db.commit()
583 # redirect to the item's edit page
584 # redirect to finish off
585 url = self.base + self.classname
586 # note that this action might have been called by an index page, so
587 # we will want to include index-page args in this URL too
588 if self.nodeid is not None:
589 url += self.nodeid
590 url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
591 urllib.quote(self.template))
592 if self.nodeid is None:
593 req = templating.HTMLRequest(self.client)
594 url += '&' + req.indexargs_url('', {})[1:]
595 raise exceptions.Redirect, url
597 class NewItemAction(EditCommon):
598 def handle(self):
599 ''' Add a new item to the database.
601 This follows the same form as the EditItemAction, with the same
602 special form values.
603 '''
604 # parse the props from the form
605 try:
606 props, links = self.client.parsePropsFromForm(create=1)
607 except (ValueError, KeyError), message:
608 self.client.error_message.append(self._('Error: %s')
609 % str(message))
610 return
612 # handle the props - edit or create
613 try:
614 # when it hits the None element, it'll set self.nodeid
615 messages = self._editnodes(props, links)
616 except (ValueError, KeyError, IndexError,
617 roundup.exceptions.Reject), message:
618 # these errors might just be indicative of user dumbness
619 self.client.error_message.append(_('Error: %s') % str(message))
620 return
622 # commit now that all the tricky stuff is done
623 self.db.commit()
625 # redirect to the new item's page
626 raise exceptions.Redirect, '%s%s%s?@ok_message=%s&@template=%s' % (
627 self.base, self.classname, self.nodeid, urllib.quote(messages),
628 urllib.quote(self.template))
630 class PassResetAction(Action):
631 def handle(self):
632 """Handle password reset requests.
634 Presence of either "name" or "address" generates email. Presence of
635 "otk" performs the reset.
637 """
638 otks = self.db.getOTKManager()
639 if self.form.has_key('otk'):
640 # pull the rego information out of the otk database
641 otk = self.form['otk'].value
642 uid = otks.get(otk, 'uid', default=None)
643 if uid is None:
644 self.client.error_message.append(
645 self._("Invalid One Time Key!\n"
646 "(a Mozilla bug may cause this message "
647 "to show up erroneously, please check your email)"))
648 return
650 # re-open the database as "admin"
651 if self.user != 'admin':
652 self.client.opendb('admin')
653 self.db = self.client.db
654 otks = self.db.getOTKManager()
656 # change the password
657 newpw = password.generatePassword()
659 cl = self.db.user
660 # XXX we need to make the "default" page be able to display errors!
661 try:
662 # set the password
663 cl.set(uid, password=password.Password(newpw))
664 # clear the props from the otk database
665 otks.destroy(otk)
666 self.db.commit()
667 except (ValueError, KeyError), message:
668 self.client.error_message.append(str(message))
669 return
671 # user info
672 address = self.db.user.get(uid, 'address')
673 name = self.db.user.get(uid, 'username')
675 # send the email
676 tracker_name = self.db.config.TRACKER_NAME
677 subject = 'Password reset for %s'%tracker_name
678 body = '''
679 The password has been reset for username "%(name)s".
681 Your password is now: %(password)s
682 '''%{'name': name, 'password': newpw}
683 if not self.client.standard_message([address], subject, body):
684 return
686 self.client.ok_message.append(
687 self._('Password reset and email sent to %s') % address)
688 return
690 # no OTK, so now figure the user
691 if self.form.has_key('username'):
692 name = self.form['username'].value
693 try:
694 uid = self.db.user.lookup(name)
695 except KeyError:
696 self.client.error_message.append(self._('Unknown username'))
697 return
698 address = self.db.user.get(uid, 'address')
699 elif self.form.has_key('address'):
700 address = self.form['address'].value
701 uid = uidFromAddress(self.db, ('', address), create=0)
702 if not uid:
703 self.client.error_message.append(
704 self._('Unknown email address'))
705 return
706 name = self.db.user.get(uid, 'username')
707 else:
708 self.client.error_message.append(
709 self._('You need to specify a username or address'))
710 return
712 # generate the one-time-key and store the props for later
713 otk = ''.join([random.choice(chars) for x in range(32)])
714 while otks.exists(otk):
715 otk = ''.join([random.choice(chars) for x in range(32)])
716 otks.set(otk, uid=uid)
717 self.db.commit()
719 # send the email
720 tracker_name = self.db.config.TRACKER_NAME
721 subject = 'Confirm reset of password for %s'%tracker_name
722 body = '''
723 Someone, perhaps you, has requested that the password be changed for your
724 username, "%(name)s". If you wish to proceed with the change, please follow
725 the link below:
727 %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
729 You should then receive another email with the new password.
730 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
731 if not self.client.standard_message([address], subject, body):
732 return
734 self.client.ok_message.append(self._('Email sent to %s') % address)
736 class RegoCommon(Action):
737 def finishRego(self):
738 # log the new user in
739 self.client.userid = self.userid
740 user = self.client.user = self.db.user.get(self.userid, 'username')
741 # re-open the database for real, using the user
742 self.client.opendb(user)
744 # update session data
745 self.client.session_api.set(user=user)
747 # nice message
748 message = self._('You are now registered, welcome!')
749 url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
750 urllib.quote(message))
752 # redirect to the user's page (but not 302, as some email clients seem
753 # to want to reload the page, or something)
754 return '''<html><head><title>%s</title></head>
755 <body><p><a href="%s">%s</a></p>
756 <script type="text/javascript">
757 window.setTimeout('window.location = "%s"', 1000);
758 </script>'''%(message, url, message, url)
760 class ConfRegoAction(RegoCommon):
761 def handle(self):
762 """Grab the OTK, use it to load up the new user details."""
763 try:
764 # pull the rego information out of the otk database
765 self.userid = self.db.confirm_registration(self.form['otk'].value)
766 except (ValueError, KeyError), message:
767 self.client.error_message.append(str(message))
768 return
769 return self.finishRego()
771 class RegisterAction(RegoCommon, EditCommon):
772 name = 'register'
773 permissionType = 'Create'
775 def handle(self):
776 """Attempt to create a new user based on the contents of the form
777 and then remember it in session.
779 Return 1 on successful login.
780 """
781 # parse the props from the form
782 try:
783 props, links = self.client.parsePropsFromForm(create=1)
784 except (ValueError, KeyError), message:
785 self.client.error_message.append(self._('Error: %s')
786 % str(message))
787 return
789 # registration isn't allowed to supply roles
790 user_props = props[('user', None)]
791 if user_props.has_key('roles'):
792 raise exceptions.Unauthorised, self._(
793 "It is not permitted to supply roles at registration.")
795 # skip the confirmation step?
796 if self.db.config['INSTANT_REGISTRATION']:
797 # handle the create now
798 try:
799 # when it hits the None element, it'll set self.nodeid
800 messages = self._editnodes(props, links)
801 except (ValueError, KeyError, IndexError,
802 roundup.exceptions.Reject), message:
803 # these errors might just be indicative of user dumbness
804 self.client.error_message.append(_('Error: %s') % str(message))
805 return
807 # fix up the initial roles
808 self.db.user.set(self.nodeid,
809 roles=self.db.config['NEW_WEB_USER_ROLES'])
811 # commit now that all the tricky stuff is done
812 self.db.commit()
814 # finish off by logging the user in
815 self.userid = self.nodeid
816 return self.finishRego()
818 # generate the one-time-key and store the props for later
819 for propname, proptype in self.db.user.getprops().items():
820 value = user_props.get(propname, None)
821 if value is None:
822 pass
823 elif isinstance(proptype, hyperdb.Date):
824 user_props[propname] = str(value)
825 elif isinstance(proptype, hyperdb.Interval):
826 user_props[propname] = str(value)
827 elif isinstance(proptype, hyperdb.Password):
828 user_props[propname] = str(value)
829 otks = self.db.getOTKManager()
830 otk = ''.join([random.choice(chars) for x in range(32)])
831 while otks.exists(otk):
832 otk = ''.join([random.choice(chars) for x in range(32)])
833 otks.set(otk, **user_props)
835 # send the email
836 tracker_name = self.db.config.TRACKER_NAME
837 tracker_email = self.db.config.TRACKER_EMAIL
838 if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
839 subject = 'Complete your registration to %s -- key %s'%(tracker_name,
840 otk)
841 body = """To complete your registration of the user "%(name)s" with
842 %(tracker)s, please do one of the following:
844 - send a reply to %(tracker_email)s and maintain the subject line as is (the
845 reply's additional "Re:" is ok),
847 - or visit the following URL:
849 %(url)s?@action=confrego&otk=%(otk)s
851 """ % {'name': user_props['username'], 'tracker': tracker_name,
852 'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
853 else:
854 subject = 'Complete your registration to %s'%(tracker_name)
855 body = """To complete your registration of the user "%(name)s" with
856 %(tracker)s, please visit the following URL:
858 %(url)s?@action=confrego&otk=%(otk)s
860 """ % {'name': user_props['username'], 'tracker': tracker_name,
861 'url': self.base, 'otk': otk}
862 if not self.client.standard_message([user_props['address']], subject,
863 body, (tracker_name, tracker_email)):
864 return
866 # commit changes to the database
867 self.db.commit()
869 # redirect to the "you're almost there" page
870 raise exceptions.Redirect, '%suser?@template=rego_progress'%self.base
872 class LogoutAction(Action):
873 def handle(self):
874 """Make us really anonymous - nuke the session too."""
875 # log us out
876 self.client.make_user_anonymous()
877 self.client.session_api.destroy()
879 # Let the user know what's going on
880 self.client.ok_message.append(self._('You are logged out'))
882 # reset client context to render tracker home page
883 # instead of last viewed page (may be inaccessibe for anonymous)
884 self.client.classname = None
885 self.client.nodeid = None
886 self.client.template = None
888 class LoginAction(Action):
889 def handle(self):
890 """Attempt to log a user in.
892 Sets up a session for the user which contains the login credentials.
894 """
895 # we need the username at a minimum
896 if not self.form.has_key('__login_name'):
897 self.client.error_message.append(self._('Username required'))
898 return
900 # get the login info
901 self.client.user = self.form['__login_name'].value
902 if self.form.has_key('__login_password'):
903 password = self.form['__login_password'].value
904 else:
905 password = ''
907 try:
908 self.verifyLogin(self.client.user, password)
909 except exceptions.LoginError, err:
910 self.client.make_user_anonymous()
911 self.client.error_message.extend(list(err.args))
912 return
914 # now we're OK, re-open the database for real, using the user
915 self.client.opendb(self.client.user)
917 # save user in session
918 self.client.session_api.set(user=self.client.user)
919 if self.form.has_key('remember'):
920 self.client.session_api.update(set_cookie=True, expire=24*3600*365)
922 # If we came from someplace, go back there
923 if self.form.has_key('__came_from'):
924 raise exceptions.Redirect, self.form['__came_from'].value
926 def verifyLogin(self, username, password):
927 # make sure the user exists
928 try:
929 self.client.userid = self.db.user.lookup(username)
930 except KeyError:
931 raise exceptions.LoginError, self._('Invalid login')
933 # verify the password
934 if not self.verifyPassword(self.client.userid, password):
935 raise exceptions.LoginError, self._('Invalid login')
937 # Determine whether the user has permission to log in.
938 # Base behaviour is to check the user has "Web Access".
939 if not self.hasPermission("Web Access"):
940 raise exceptions.LoginError, self._(
941 "You do not have permission to login")
943 def verifyPassword(self, userid, password):
944 '''Verify the password that the user has supplied'''
945 stored = self.db.user.get(userid, 'password')
946 if password == stored:
947 return 1
948 if not password and not stored:
949 return 1
950 return 0
952 class ExportCSVAction(Action):
953 name = 'export'
954 permissionType = 'View'
956 def handle(self):
957 ''' Export the specified search query as CSV. '''
958 # figure the request
959 request = templating.HTMLRequest(self.client)
960 filterspec = request.filterspec
961 sort = request.sort
962 group = request.group
963 columns = request.columns
964 klass = self.db.getclass(request.classname)
966 # full-text search
967 if request.search_text:
968 matches = self.db.indexer.search(
969 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
970 else:
971 matches = None
973 h = self.client.additional_headers
974 h['Content-Type'] = 'text/csv; charset=%s' % self.client.charset
975 # some browsers will honor the filename here...
976 h['Content-Disposition'] = 'inline; filename=query.csv'
978 self.client.header()
980 if self.client.env['REQUEST_METHOD'] == 'HEAD':
981 # all done, return a dummy string
982 return 'dummy'
984 wfile = self.client.request.wfile
985 if self.client.charset != self.client.STORAGE_CHARSET:
986 wfile = codecs.EncodedFile(wfile,
987 self.client.STORAGE_CHARSET, self.client.charset, 'replace')
989 writer = csv.writer(wfile)
990 self.client._socket_op(writer.writerow, columns)
992 # and search
993 for itemid in klass.filter(matches, filterspec, sort, group):
994 self.client._socket_op(writer.writerow, [str(klass.get(itemid, col)) for col in columns])
996 return '\n'
998 # vim: set filetype=python sts=4 sw=4 et si :