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, property=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, property=property)
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."""
490 if itemid is None:
491 itemid = self.nodeid
492 if classname is self._cn_marker:
493 classname = self.classname
494 # The user must have permission to edit each of the properties
495 # being changed.
496 for p in props:
497 if not self.hasPermission('Edit',
498 itemid=itemid,
499 classname=classname,
500 property=p):
501 return 0
502 # Since the user has permission to edit all of the properties,
503 # the edit is OK.
504 return 1
506 def newItemPermission(self, props, classname=None):
507 """Determine whether the user has permission to create this item.
509 Base behaviour is to check the user can edit this class. No additional
510 property checks are made.
511 """
512 if not classname :
513 classname = self.client.classname
514 return self.hasPermission('Create', classname=classname)
516 class EditItemAction(EditCommon):
517 def lastUserActivity(self):
518 if self.form.has_key(':lastactivity'):
519 d = date.Date(self.form[':lastactivity'].value)
520 elif self.form.has_key('@lastactivity'):
521 d = date.Date(self.form['@lastactivity'].value)
522 else:
523 return None
524 d.second = int(d.second)
525 return d
527 def lastNodeActivity(self):
528 cl = getattr(self.client.db, self.classname)
529 activity = cl.get(self.nodeid, 'activity').local(0)
530 activity.second = int(activity.second)
531 return activity
533 def detectCollision(self, user_activity, node_activity):
534 '''Check for a collision and return the list of props we edited
535 that conflict.'''
536 if user_activity and user_activity < node_activity:
537 props, links = self.client.parsePropsFromForm()
538 key = (self.classname, self.nodeid)
539 # we really only collide for direct prop edit conflicts
540 return props[key].keys()
541 else:
542 return []
544 def handleCollision(self, props):
545 message = self._('Edit Error: someone else has edited this %s (%s). '
546 'View <a target="new" href="%s%s">their changes</a> '
547 'in a new window.')%(self.classname, ', '.join(props),
548 self.classname, self.nodeid)
549 self.client.error_message.append(message)
550 return
552 def handle(self):
553 """Perform an edit of an item in the database.
555 See parsePropsFromForm and _editnodes for special variables.
557 """
558 user_activity = self.lastUserActivity()
559 if user_activity:
560 props = self.detectCollision(user_activity, self.lastNodeActivity())
561 if props:
562 self.handleCollision(props)
563 return
565 props, links = self.client.parsePropsFromForm()
567 # handle the props
568 try:
569 message = self._editnodes(props, links)
570 except (ValueError, KeyError, IndexError,
571 roundup.exceptions.Reject), message:
572 self.client.error_message.append(
573 self._('Edit Error: %s') % str(message))
574 return
576 # commit now that all the tricky stuff is done
577 self.db.commit()
579 # redirect to the item's edit page
580 # redirect to finish off
581 url = self.base + self.classname
582 # note that this action might have been called by an index page, so
583 # we will want to include index-page args in this URL too
584 if self.nodeid is not None:
585 url += self.nodeid
586 url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
587 urllib.quote(self.template))
588 if self.nodeid is None:
589 req = templating.HTMLRequest(self.client)
590 url += '&' + req.indexargs_url('', {})[1:]
591 raise exceptions.Redirect, url
593 class NewItemAction(EditCommon):
594 def handle(self):
595 ''' Add a new item to the database.
597 This follows the same form as the EditItemAction, with the same
598 special form values.
599 '''
600 # parse the props from the form
601 try:
602 props, links = self.client.parsePropsFromForm(create=1)
603 except (ValueError, KeyError), message:
604 self.client.error_message.append(self._('Error: %s')
605 % str(message))
606 return
608 # handle the props - edit or create
609 try:
610 # when it hits the None element, it'll set self.nodeid
611 messages = self._editnodes(props, links)
612 except (ValueError, KeyError, IndexError,
613 roundup.exceptions.Reject), message:
614 # these errors might just be indicative of user dumbness
615 self.client.error_message.append(_('Error: %s') % str(message))
616 return
618 # commit now that all the tricky stuff is done
619 self.db.commit()
621 # redirect to the new item's page
622 raise exceptions.Redirect, '%s%s%s?@ok_message=%s&@template=%s' % (
623 self.base, self.classname, self.nodeid, urllib.quote(messages),
624 urllib.quote(self.template))
626 class PassResetAction(Action):
627 def handle(self):
628 """Handle password reset requests.
630 Presence of either "name" or "address" generates email. Presence of
631 "otk" performs the reset.
633 """
634 otks = self.db.getOTKManager()
635 if self.form.has_key('otk'):
636 # pull the rego information out of the otk database
637 otk = self.form['otk'].value
638 uid = otks.get(otk, 'uid', default=None)
639 if uid is None:
640 self.client.error_message.append(
641 self._("Invalid One Time Key!\n"
642 "(a Mozilla bug may cause this message "
643 "to show up erroneously, please check your email)"))
644 return
646 # re-open the database as "admin"
647 if self.user != 'admin':
648 self.client.opendb('admin')
649 self.db = self.client.db
650 otks = self.db.getOTKManager()
652 # change the password
653 newpw = password.generatePassword()
655 cl = self.db.user
656 # XXX we need to make the "default" page be able to display errors!
657 try:
658 # set the password
659 cl.set(uid, password=password.Password(newpw))
660 # clear the props from the otk database
661 otks.destroy(otk)
662 self.db.commit()
663 except (ValueError, KeyError), message:
664 self.client.error_message.append(str(message))
665 return
667 # user info
668 address = self.db.user.get(uid, 'address')
669 name = self.db.user.get(uid, 'username')
671 # send the email
672 tracker_name = self.db.config.TRACKER_NAME
673 subject = 'Password reset for %s'%tracker_name
674 body = '''
675 The password has been reset for username "%(name)s".
677 Your password is now: %(password)s
678 '''%{'name': name, 'password': newpw}
679 if not self.client.standard_message([address], subject, body):
680 return
682 self.client.ok_message.append(
683 self._('Password reset and email sent to %s') % address)
684 return
686 # no OTK, so now figure the user
687 if self.form.has_key('username'):
688 name = self.form['username'].value
689 try:
690 uid = self.db.user.lookup(name)
691 except KeyError:
692 self.client.error_message.append(self._('Unknown username'))
693 return
694 address = self.db.user.get(uid, 'address')
695 elif self.form.has_key('address'):
696 address = self.form['address'].value
697 uid = uidFromAddress(self.db, ('', address), create=0)
698 if not uid:
699 self.client.error_message.append(
700 self._('Unknown email address'))
701 return
702 name = self.db.user.get(uid, 'username')
703 else:
704 self.client.error_message.append(
705 self._('You need to specify a username or address'))
706 return
708 # generate the one-time-key and store the props for later
709 otk = ''.join([random.choice(chars) for x in range(32)])
710 while otks.exists(otk):
711 otk = ''.join([random.choice(chars) for x in range(32)])
712 otks.set(otk, uid=uid)
713 self.db.commit()
715 # send the email
716 tracker_name = self.db.config.TRACKER_NAME
717 subject = 'Confirm reset of password for %s'%tracker_name
718 body = '''
719 Someone, perhaps you, has requested that the password be changed for your
720 username, "%(name)s". If you wish to proceed with the change, please follow
721 the link below:
723 %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
725 You should then receive another email with the new password.
726 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
727 if not self.client.standard_message([address], subject, body):
728 return
730 self.client.ok_message.append(self._('Email sent to %s') % address)
732 class RegoCommon(Action):
733 def finishRego(self):
734 # log the new user in
735 self.client.userid = self.userid
736 user = self.client.user = self.db.user.get(self.userid, 'username')
737 # re-open the database for real, using the user
738 self.client.opendb(user)
740 # update session data
741 self.client.session_api.set(user=user)
743 # nice message
744 message = self._('You are now registered, welcome!')
745 url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
746 urllib.quote(message))
748 # redirect to the user's page (but not 302, as some email clients seem
749 # to want to reload the page, or something)
750 return '''<html><head><title>%s</title></head>
751 <body><p><a href="%s">%s</a></p>
752 <script type="text/javascript">
753 window.setTimeout('window.location = "%s"', 1000);
754 </script>'''%(message, url, message, url)
756 class ConfRegoAction(RegoCommon):
757 def handle(self):
758 """Grab the OTK, use it to load up the new user details."""
759 try:
760 # pull the rego information out of the otk database
761 self.userid = self.db.confirm_registration(self.form['otk'].value)
762 except (ValueError, KeyError), message:
763 self.client.error_message.append(str(message))
764 return
765 return self.finishRego()
767 class RegisterAction(RegoCommon, EditCommon):
768 name = 'register'
769 permissionType = 'Create'
771 def handle(self):
772 """Attempt to create a new user based on the contents of the form
773 and then remember it in session.
775 Return 1 on successful login.
776 """
777 # parse the props from the form
778 try:
779 props, links = self.client.parsePropsFromForm(create=1)
780 except (ValueError, KeyError), message:
781 self.client.error_message.append(self._('Error: %s')
782 % str(message))
783 return
785 # registration isn't allowed to supply roles
786 user_props = props[('user', None)]
787 if user_props.has_key('roles'):
788 raise exceptions.Unauthorised, self._(
789 "It is not permitted to supply roles at registration.")
791 # skip the confirmation step?
792 if self.db.config['INSTANT_REGISTRATION']:
793 # handle the create now
794 try:
795 # when it hits the None element, it'll set self.nodeid
796 messages = self._editnodes(props, links)
797 except (ValueError, KeyError, IndexError,
798 roundup.exceptions.Reject), message:
799 # these errors might just be indicative of user dumbness
800 self.client.error_message.append(_('Error: %s') % str(message))
801 return
803 # fix up the initial roles
804 self.db.user.set(self.nodeid,
805 roles=self.db.config['NEW_WEB_USER_ROLES'])
807 # commit now that all the tricky stuff is done
808 self.db.commit()
810 # finish off by logging the user in
811 self.userid = self.nodeid
812 return self.finishRego()
814 # generate the one-time-key and store the props for later
815 for propname, proptype in self.db.user.getprops().items():
816 value = user_props.get(propname, None)
817 if value is None:
818 pass
819 elif isinstance(proptype, hyperdb.Date):
820 user_props[propname] = str(value)
821 elif isinstance(proptype, hyperdb.Interval):
822 user_props[propname] = str(value)
823 elif isinstance(proptype, hyperdb.Password):
824 user_props[propname] = str(value)
825 otks = self.db.getOTKManager()
826 otk = ''.join([random.choice(chars) for x in range(32)])
827 while otks.exists(otk):
828 otk = ''.join([random.choice(chars) for x in range(32)])
829 otks.set(otk, **user_props)
831 # send the email
832 tracker_name = self.db.config.TRACKER_NAME
833 tracker_email = self.db.config.TRACKER_EMAIL
834 if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
835 subject = 'Complete your registration to %s -- key %s'%(tracker_name,
836 otk)
837 body = """To complete your registration of the user "%(name)s" with
838 %(tracker)s, please do one of the following:
840 - send a reply to %(tracker_email)s and maintain the subject line as is (the
841 reply's additional "Re:" is ok),
843 - or visit the following URL:
845 %(url)s?@action=confrego&otk=%(otk)s
847 """ % {'name': user_props['username'], 'tracker': tracker_name,
848 'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
849 else:
850 subject = 'Complete your registration to %s'%(tracker_name)
851 body = """To complete your registration of the user "%(name)s" with
852 %(tracker)s, please visit the following URL:
854 %(url)s?@action=confrego&otk=%(otk)s
856 """ % {'name': user_props['username'], 'tracker': tracker_name,
857 'url': self.base, 'otk': otk}
858 if not self.client.standard_message([user_props['address']], subject,
859 body, (tracker_name, tracker_email)):
860 return
862 # commit changes to the database
863 self.db.commit()
865 # redirect to the "you're almost there" page
866 raise exceptions.Redirect, '%suser?@template=rego_progress'%self.base
868 class LogoutAction(Action):
869 def handle(self):
870 """Make us really anonymous - nuke the session too."""
871 # log us out
872 self.client.make_user_anonymous()
873 self.client.session_api.destroy()
875 # Let the user know what's going on
876 self.client.ok_message.append(self._('You are logged out'))
878 # reset client context to render tracker home page
879 # instead of last viewed page (may be inaccessibe for anonymous)
880 self.client.classname = None
881 self.client.nodeid = None
882 self.client.template = None
884 class LoginAction(Action):
885 def handle(self):
886 """Attempt to log a user in.
888 Sets up a session for the user which contains the login credentials.
890 """
891 # we need the username at a minimum
892 if not self.form.has_key('__login_name'):
893 self.client.error_message.append(self._('Username required'))
894 return
896 # get the login info
897 self.client.user = self.form['__login_name'].value
898 if self.form.has_key('__login_password'):
899 password = self.form['__login_password'].value
900 else:
901 password = ''
903 try:
904 self.verifyLogin(self.client.user, password)
905 except exceptions.LoginError, err:
906 self.client.make_user_anonymous()
907 self.client.error_message.extend(list(err.args))
908 return
910 # now we're OK, re-open the database for real, using the user
911 self.client.opendb(self.client.user)
913 # save user in session
914 self.client.session_api.set(user=self.client.user)
915 if self.form.has_key('remember'):
916 self.client.session_api.update(set_cookie=True, expire=24*3600*365)
918 # If we came from someplace, go back there
919 if self.form.has_key('__came_from'):
920 raise exceptions.Redirect, self.form['__came_from'].value
922 def verifyLogin(self, username, password):
923 # make sure the user exists
924 try:
925 self.client.userid = self.db.user.lookup(username)
926 except KeyError:
927 raise exceptions.LoginError, self._('Invalid login')
929 # verify the password
930 if not self.verifyPassword(self.client.userid, password):
931 raise exceptions.LoginError, self._('Invalid login')
933 # Determine whether the user has permission to log in.
934 # Base behaviour is to check the user has "Web Access".
935 if not self.hasPermission("Web Access"):
936 raise exceptions.LoginError, self._(
937 "You do not have permission to login")
939 def verifyPassword(self, userid, password):
940 '''Verify the password that the user has supplied'''
941 stored = self.db.user.get(userid, 'password')
942 if password == stored:
943 return 1
944 if not password and not stored:
945 return 1
946 return 0
948 class ExportCSVAction(Action):
949 name = 'export'
950 permissionType = 'View'
952 def handle(self):
953 ''' Export the specified search query as CSV. '''
954 # figure the request
955 request = templating.HTMLRequest(self.client)
956 filterspec = request.filterspec
957 sort = request.sort
958 group = request.group
959 columns = request.columns
960 klass = self.db.getclass(request.classname)
962 # full-text search
963 if request.search_text:
964 matches = self.db.indexer.search(
965 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
966 else:
967 matches = None
969 h = self.client.additional_headers
970 h['Content-Type'] = 'text/csv; charset=%s' % self.client.charset
971 # some browsers will honor the filename here...
972 h['Content-Disposition'] = 'inline; filename=query.csv'
974 self.client.header()
976 if self.client.env['REQUEST_METHOD'] == 'HEAD':
977 # all done, return a dummy string
978 return 'dummy'
980 wfile = self.client.request.wfile
981 if self.client.charset != self.client.STORAGE_CHARSET:
982 wfile = codecs.EncodedFile(wfile,
983 self.client.STORAGE_CHARSET, self.client.charset, 'replace')
985 writer = csv.writer(wfile)
986 self.client._socket_op(writer.writerow, columns)
988 # and search
989 for itemid in klass.filter(matches, filterspec, sort, group):
990 self.client._socket_op(writer.writerow, [str(klass.get(itemid, col)) for col in columns])
992 return '\n'
994 # vim: set filetype=python sts=4 sw=4 et si :