Code

ac43e4c0a91fe1164b6e1379bf01c695607738db
[roundup.git] / roundup / cgi / actions.py
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 :