diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py
index 4cf09927736e9d217105f2e55f42c08025e46dac..c19849413aff73dc169ead6b0e6ba347f8996267 100755 (executable)
--- a/roundup/cgi/actions.py
+++ b/roundup/cgi/actions.py
-import re, cgi, StringIO, urllib, Cookie, time, random
+import re, cgi, StringIO, urllib, time, random, csv, codecs
-from roundup import hyperdb, token, date, password, rcsv
+from roundup import hyperdb, token, date, password
+from roundup.actions import Action as BaseAction
from roundup.i18n import _
-from roundup.cgi import templating
-from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError
+import roundup.exceptions
+from roundup.cgi import exceptions, templating
from roundup.mailgw import uidFromAddress
__all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
'EditCSVAction', 'EditItemAction', 'PassResetAction',
'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
- 'NewItemAction']
+ 'NewItemAction', 'ExportCSVAction']
# used by a couple of routines
chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
self.userid = client.userid
self.base = client.base
self.user = client.user
+ self.context = templating.context(client)
+
+ def handle(self):
+ """Action handler procedure"""
+ raise NotImplementedError
def execute(self):
"""Execute the action specified by this object."""
self.permission()
- self.handle()
+ return self.handle()
name = ''
permissionType = None
if (self.permissionType and
not self.hasPermission(self.permissionType)):
info = {'action': self.name, 'classname': self.classname}
- raise Unauthorised, _('You do not have permission to '
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to '
'%(action)s the %(classname)s class.')%info
- def hasPermission(self, permission):
+ _marker = []
+ def hasPermission(self, permission, classname=_marker, itemid=None, property=None):
"""Check whether the user has 'permission' on the current class."""
+ if classname is self._marker:
+ classname = self.client.classname
return self.db.security.hasPermission(permission, self.client.userid,
- self.client.classname)
+ classname=classname, itemid=itemid, property=property)
+
+ def gettext(self, msgid):
+ """Return the localized translation of msgid"""
+ return self.client.translator.gettext(msgid)
+
+ _ = gettext
class ShowAction(Action):
- def handle(self, typere=re.compile('[@:]type'),
- numre=re.compile('[@:]number')):
+
+ typere=re.compile('[@:]type')
+ numre=re.compile('[@:]number')
+
+ def handle(self):
"""Show a node of a particular class/id."""
t = n = ''
for key in self.form.keys():
- if typere.match(key):
+ if self.typere.match(key):
t = self.form[key].value.strip()
- elif numre.match(key):
+ elif self.numre.match(key):
n = self.form[key].value.strip()
if not t:
- raise ValueError, 'No type specified'
+ raise ValueError, self._('No type specified')
if not n:
- raise SeriousError, _('No ID entered')
+ raise exceptions.SeriousError, self._('No ID entered')
try:
int(n)
except ValueError:
d = {'input': n, 'classname': t}
- raise SeriousError, _(
+ raise exceptions.SeriousError, self._(
'"%(input)s" is not an ID (%(classname)s ID required)')%d
- url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n)
- raise Redirect, url
+ url = '%s%s%s'%(self.base, t, n)
+ raise exceptions.Redirect, url
class RetireAction(Action):
name = 'retire'
def handle(self):
"""Retire the context item."""
- # if we want to view the index template now, then unset the nodeid
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+
+ # if we want to view the index template now, then unset the itemid
# context info (a special-case for retire actions on the index page)
- nodeid = self.nodeid
+ itemid = self.nodeid
if self.template == 'index':
self.client.nodeid = None
# make sure we don't try to retire admin or anonymous
if self.classname == 'user' and \
- self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
- raise ValueError, _('You may not retire the admin or anonymous user')
+ self.db.user.get(itemid, 'username') in ('admin', 'anonymous'):
+ raise ValueError, self._(
+ 'You may not retire the admin or anonymous user')
+
+ # check permission
+ if not self.hasPermission('Retire', classname=self.classname,
+ itemid=itemid):
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to retire %(class)s'
+ ) % {'class': self.classname}
# do the retire
- self.db.getclass(self.classname).retire(nodeid)
+ self.db.getclass(self.classname).retire(itemid)
self.db.commit()
self.client.ok_message.append(
- _('%(classname)s %(itemid)s has been retired')%{
- 'classname': self.classname.capitalize(), 'itemid': nodeid})
+ self._('%(classname)s %(itemid)s has been retired')%{
+ 'classname': self.classname.capitalize(), 'itemid': itemid})
+
class SearchAction(Action):
name = 'search'
permissionType = 'View'
- def handle(self, wcre=re.compile(r'[\s,]+')):
+ def handle(self):
"""Mangle some of the form variables.
Set the form ":filter" variable based on the values of the filter
self.fakeFilterVars()
queryname = self.getQueryName()
+ # editing existing query name?
+ old_queryname = self.getFromForm('old-queryname')
+
# handle saving the query params
if queryname:
# parse the environment and figure what the query _is_
req = templating.HTMLRequest(self.client)
- # The [1:] strips off the '?' character, it isn't part of the
- # query string.
- url = req.indexargs_href('', {})[1:]
-
- # handle editing an existing query
- try:
- qid = self.db.query.lookup(queryname)
- self.db.query.set(qid, klass=self.classname, url=url)
- except KeyError:
- # create a query
- qid = self.db.query.create(name=queryname,
- klass=self.classname, url=url)
+ url = self.getCurrentURL(req)
+
+ key = self.db.query.getkey()
+ if key:
+ # edit the old way, only one query per name
+ try:
+ qid = self.db.query.lookup(old_queryname)
+ if not self.hasPermission('Edit', 'query', itemid=qid):
+ raise exceptions.Unauthorised, self._(
+ "You do not have permission to edit queries")
+ self.db.query.set(qid, klass=self.classname, url=url)
+ except KeyError:
+ # create a query
+ if not self.hasPermission('Create', 'query'):
+ raise exceptions.Unauthorised, self._(
+ "You do not have permission to store queries")
+ qid = self.db.query.create(name=queryname,
+ klass=self.classname, url=url)
+ else:
+ # edit the new way, query name not a key any more
+ # see if we match an existing private query
+ uid = self.db.getuid()
+ qids = self.db.query.filter(None, {'name': old_queryname,
+ 'private_for': uid})
+ if not qids:
+ # ok, so there's not a private query for the current user
+ # - see if there's one created by them
+ qids = self.db.query.filter(None, {'name': old_queryname,
+ 'creator': uid})
+
+ if qids and old_queryname:
+ # edit query - make sure we get an exact match on the name
+ for qid in qids:
+ if old_queryname != self.db.query.get(qid, 'name'):
+ continue
+ if not self.hasPermission('Edit', 'query', itemid=qid):
+ raise exceptions.Unauthorised, self._(
+ "You do not have permission to edit queries")
+ self.db.query.set(qid, klass=self.classname,
+ url=url, name=queryname)
+ else:
+ # create a query
+ if not self.hasPermission('Create', 'query'):
+ raise exceptions.Unauthorised, self._(
+ "You do not have permission to store queries")
+ qid = self.db.query.create(name=queryname,
+ klass=self.classname, url=url, private_for=uid)
# and add it to the user's query multilink
queries = self.db.user.get(self.userid, 'queries')
def fakeFilterVars(self):
"""Add a faked :filter form variable for each filtering prop."""
- props = self.db.classes[self.classname].getprops()
+ cls = self.db.classes[self.classname]
for key in self.form.keys():
- if not props.has_key(key):
+ prop = cls.get_transitive_prop(key)
+ if not prop:
continue
if isinstance(self.form[key], type([])):
# search for at least one entry which is not empty
else:
if not self.form[key].value:
continue
- if isinstance(props[key], hyperdb.String):
+ if isinstance(prop, hyperdb.String):
v = self.form[key].value
l = token.token_split(v)
- if len(l) > 1 or l[0] != v:
+ if len(l) != 1 or l[0] != v:
self.form.value.remove(self.form[key])
# replace the single value with the split list
for v in l:
self.form.value.append(cgi.MiniFieldStorage('@filter', key))
- FV_QUERYNAME = re.compile(r'[@:]queryname')
- def getQueryName(self):
- for key in self.form.keys():
- if self.FV_QUERYNAME.match(key):
+ def getCurrentURL(self, req):
+ """Get current URL for storing as a query.
+
+ Note: We are removing the first character from the current URL,
+ because the leading '?' is not part of the query string.
+
+ Implementation note:
+ But maybe the template should be part of the stored query:
+ template = self.getFromForm('template')
+ if template:
+ return req.indexargs_url('', {'@template' : template})[1:]
+ """
+ return req.indexargs_url('', {})[1:]
+
+ def getFromForm(self, name):
+ for key in ('@' + name, ':' + name):
+ if self.form.has_key(key):
return self.form[key].value.strip()
return ''
+ def getQueryName(self):
+ return self.getFromForm('queryname')
+
class EditCSVAction(Action):
name = 'edit'
permissionType = 'Edit'
The "rows" CGI var defines the CSV-formatted entries for the class. New
nodes are identified by the ID 'X' (or any other non-existent ID) and
removed lines are retired.
-
"""
- # get the CSV module
- if rcsv.error:
- self.client.error_message.append(_(rcsv.error))
- return
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+ # figure the properties list for the class
cl = self.db.classes[self.classname]
- idlessprops = cl.getprops(protected=0).keys()
- idlessprops.sort()
- props = ['id'] + idlessprops
+ props_without_id = cl.getprops(protected=0).keys()
+
+ # the incoming CSV data will always have the properties in colums
+ # sorted and starting with the "id" column
+ props_without_id.sort()
+ props = ['id'] + props_without_id
# do the edit
rows = StringIO.StringIO(self.form['rows'].value)
- reader = rcsv.reader(rows, rcsv.comma_separated)
+ reader = csv.reader(rows)
found = {}
line = 0
for values in reader:
if values == props:
continue
- # extract the nodeid
- nodeid, values = values[0], values[1:]
- found[nodeid] = 1
+ # extract the itemid
+ itemid, values = values[0], values[1:]
+ found[itemid] = 1
# see if the node exists
- if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
+ if itemid in ('x', 'X') or not cl.hasnode(itemid):
exists = 0
+
+ # check permission to create this item
+ if not self.hasPermission('Create', classname=self.classname):
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to create %(class)s'
+ ) % {'class': self.classname}
+ elif cl.hasnode(itemid) and cl.is_retired(itemid):
+ # If a CSV line just mentions an id and the corresponding
+ # item is retired, then the item is restored.
+ cl.restore(itemid)
+ continue
else:
exists = 1
# confirm correct weight
- if len(idlessprops) != len(values):
+ if len(props_without_id) != len(values):
self.client.error_message.append(
- _('Not enough values on line %(line)s')%{'line':line})
+ self._('Not enough values on line %(line)s')%{'line':line})
return
# extract the new values
d = {}
- for name, value in zip(idlessprops, values):
+ for name, value in zip(props_without_id, values):
+ # check permission to edit this property on this item
+ if exists and not self.hasPermission('Edit', itemid=itemid,
+ classname=self.classname, property=name):
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to edit %(class)s'
+ ) % {'class': self.classname}
+
prop = cl.properties[name]
value = value.strip()
# only add the property if it has a value
# perform the edit
if exists:
# edit existing
- cl.set(nodeid, **d)
+ cl.set(itemid, **d)
else:
# new node
found[cl.create(**d)] = 1
# retire the removed entries
- for nodeid in cl.list():
- if not found.has_key(nodeid):
- cl.retire(nodeid)
+ for itemid in cl.list():
+ if not found.has_key(itemid):
+ # check permission to retire this item
+ if not self.hasPermission('Retire', itemid=itemid,
+ classname=self.classname):
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to retire %(class)s'
+ ) % {'class': self.classname}
+ cl.retire(itemid)
# all OK
self.db.commit()
- self.client.ok_message.append(_('Items edited OK'))
+ self.client.ok_message.append(self._('Items edited OK'))
-class _EditAction(Action):
- def isEditingSelf(self):
- """Check whether a user is editing his/her own details."""
- return (self.nodeid == self.userid
- and self.db.user.get(self.nodeid, 'username') != 'anonymous')
-
- def editItemPermission(self, props):
- """Determine whether the user has permission to edit this item.
+class EditCommon(Action):
+ '''Utility methods for editing.'''
- Base behaviour is to check the user can edit this class. If we're
- editing the "user" class, users are allowed to edit their own details.
- Unless it's the "roles" property, which requires the special Permission
- "Web Roles".
- """
- if self.classname == 'user':
- if props.has_key('roles') and not self.hasPermission('Web Roles'):
- raise Unauthorised, _("You do not have permission to edit user roles")
- if self.isEditingSelf():
- return 1
- if self.hasPermission('Edit'):
- return 1
- return 0
-
- def newItemPermission(self, props):
- """Determine whether the user has permission to create (edit) this item.
-
- Base behaviour is to check the user can edit this class. No additional
- property checks are made. Additionally, new user items may be created
- if the user has the "Web Registration" Permission.
-
- """
- if (self.classname == 'user' and self.hasPermission('Web Registration')
- or self.hasPermission('Edit')):
- return 1
- return 0
-
- #
- # Utility methods for editing
- #
- def _editnodes(self, all_props, all_links, newids=None):
+ def _editnodes(self, all_props, all_links):
''' Use the props in all_props to perform edit and creation, then
use the link specs in all_links to do linking.
'''
deps = {}
links = {}
for cn, nodeid, propname, vlist in all_links:
- if not all_props.has_key((cn, nodeid)):
+ numeric_id = int (nodeid or 0)
+ if not (numeric_id > 0 or all_props.has_key((cn, nodeid))):
# link item to link to doesn't (and won't) exist
continue
+
for value in vlist:
if not all_props.has_key(value):
# link item to link to doesn't (and won't) exist
m = []
for needed in order:
props = all_props[needed]
- if not props:
- # nothing to do
- continue
cn, nodeid = needed
-
- if nodeid is not None and int(nodeid) > 0:
- # make changes to the node
- props = self._changenode(cn, nodeid, props)
-
- # and some nice feedback for the user
- if props:
- info = ', '.join(props.keys())
- m.append('%s %s %s edited ok'%(cn, nodeid, info))
+ if props:
+ if nodeid is not None and int(nodeid) > 0:
+ # make changes to the node
+ props = self._changenode(cn, nodeid, props)
+
+ # and some nice feedback for the user
+ if props:
+ info = ', '.join(map(self._, props.keys()))
+ m.append(
+ self._('%(class)s %(id)s %(properties)s edited ok')
+ % {'class':cn, 'id':nodeid, 'properties':info})
+ else:
+ m.append(self._('%(class)s %(id)s - nothing changed')
+ % {'class':cn, 'id':nodeid})
else:
- m.append('%s %s - nothing changed'%(cn, nodeid))
- else:
- assert props
+ assert props
- # make a new node
- newid = self._createnode(cn, props)
- if nodeid is None:
- self.nodeid = newid
- nodeid = newid
+ # make a new node
+ newid = self._createnode(cn, props)
+ if nodeid is None:
+ self.nodeid = newid
+ nodeid = newid
- # and some nice feedback for the user
- m.append('%s %s created'%(cn, newid))
+ # and some nice feedback for the user
+ m.append(self._('%(class)s %(id)s created')
+ % {'class':cn, 'id':newid})
# fill in new ids in links
if links.has_key(needed):
if linkid is None or linkid.startswith('-'):
# linking to a new item
if isinstance(propdef, hyperdb.Multilink):
- props[linkprop] = [newid]
+ props[linkprop] = [nodeid]
else:
- props[linkprop] = newid
+ props[linkprop] = nodeid
else:
# linking to an existing item
if isinstance(propdef, hyperdb.Multilink):
existing.append(nodeid)
props[linkprop] = existing
else:
- props[linkprop] = newid
+ props[linkprop] = nodeid
return '<br>'.join(m)
def _changenode(self, cn, nodeid, props):
"""Change the node based on the contents of the form."""
# check for permission
- if not self.editItemPermission(props):
- raise Unauthorised, 'You do not have permission to edit %s'%cn
+ if not self.editItemPermission(props, classname=cn, itemid=nodeid):
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to edit %(class)s'
+ ) % {'class': cn}
# make the changes
cl = self.db.classes[cn]
def _createnode(self, cn, props):
"""Create a node based on the contents of the form."""
# check for permission
- if not self.newItemPermission(props):
- raise Unauthorised, 'You do not have permission to create %s'%cn
+ if not self.newItemPermission(props, classname=cn):
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to create %(class)s'
+ ) % {'class': cn}
# create the node and return its id
cl = self.db.classes[cn]
return cl.create(**props)
-class EditItemAction(_EditAction):
+ def isEditingSelf(self):
+ """Check whether a user is editing his/her own details."""
+ return (self.nodeid == self.userid
+ and self.db.user.get(self.nodeid, 'username') != 'anonymous')
+
+ _cn_marker = []
+ def editItemPermission(self, props, classname=_cn_marker, itemid=None):
+ """Determine whether the user has permission to edit this item."""
+ if itemid is None:
+ itemid = self.nodeid
+ if classname is self._cn_marker:
+ classname = self.classname
+ # The user must have permission to edit each of the properties
+ # being changed.
+ for p in props:
+ if not self.hasPermission('Edit', itemid=itemid,
+ classname=classname, property=p):
+ return 0
+ # Since the user has permission to edit all of the properties,
+ # the edit is OK.
+ return 1
+
+ def newItemPermission(self, props, classname=None):
+ """Determine whether the user has permission to create this item.
+
+ Base behaviour is to check the user can edit this class. No additional
+ property checks are made.
+ """
+
+ if not classname :
+ classname = self.client.classname
+
+ if not self.hasPermission('Create', classname=classname):
+ return 0
+
+ # Check Edit permission for each property, to avoid being able
+ # to set restricted ones on new item creation
+ for key in props:
+ if not self.hasPermission('Edit', classname=classname,
+ property=key):
+ # We restrict by default and special-case allowed properties
+ if key == 'date' or key == 'content':
+ continue
+ elif key == 'author' and props[key] == self.userid:
+ continue
+ return 0
+ return 1
+
+class EditItemAction(EditCommon):
def lastUserActivity(self):
if self.form.has_key(':lastactivity'):
- return date.Date(self.form[':lastactivity'].value)
+ d = date.Date(self.form[':lastactivity'].value)
elif self.form.has_key('@lastactivity'):
- return date.Date(self.form['@lastactivity'].value)
+ d = date.Date(self.form['@lastactivity'].value)
else:
return None
+ d.second = int(d.second)
+ return d
def lastNodeActivity(self):
cl = getattr(self.client.db, self.classname)
- return cl.get(self.nodeid, 'activity')
-
- def detectCollision(self, userActivity, nodeActivity):
- # Result from lastUserActivity may be None. If it is, assume there's no
- # conflict, or at least not one we can detect.
- if userActivity:
- return userActivity < nodeActivity
+ activity = cl.get(self.nodeid, 'activity').local(0)
+ activity.second = int(activity.second)
+ return activity
+
+ def detectCollision(self, user_activity, node_activity):
+ '''Check for a collision and return the list of props we edited
+ that conflict.'''
+ if user_activity and user_activity < node_activity:
+ props, links = self.client.parsePropsFromForm()
+ key = (self.classname, self.nodeid)
+ # we really only collide for direct prop edit conflicts
+ return props[key].keys()
+ else:
+ return []
- def handleCollision(self):
- self.client.template = 'collision'
+ def handleCollision(self, props):
+ message = self._('Edit Error: someone else has edited this %s (%s). '
+ 'View <a target="new" href="%s%s">their changes</a> '
+ 'in a new window.')%(self.classname, ', '.join(props),
+ self.classname, self.nodeid)
+ self.client.error_message.append(message)
+ return
def handle(self):
"""Perform an edit of an item in the database.
See parsePropsFromForm and _editnodes for special variables.
"""
- if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
- self.handleCollision()
- return
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+
+ user_activity = self.lastUserActivity()
+ if user_activity:
+ props = self.detectCollision(user_activity, self.lastNodeActivity())
+ if props:
+ self.handleCollision(props)
+ return
props, links = self.client.parsePropsFromForm()
# handle the props
try:
message = self._editnodes(props, links)
- except (ValueError, KeyError, IndexError), message:
- self.client.error_message.append(_('Apply Error: ') + str(message))
+ except (ValueError, KeyError, IndexError,
+ roundup.exceptions.Reject), message:
+ self.client.error_message.append(
+ self._('Edit Error: %s') % str(message))
return
# commit now that all the tricky stuff is done
url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
urllib.quote(self.template))
if self.nodeid is None:
- req = templating.HTMLRequest(self)
- url += '&' + req.indexargs_href('', {})[1:]
- raise Redirect, url
+ req = templating.HTMLRequest(self.client)
+ url += '&' + req.indexargs_url('', {})[1:]
+ raise exceptions.Redirect, url
-class NewItemAction(_EditAction):
+class NewItemAction(EditCommon):
def handle(self):
''' Add a new item to the database.
This follows the same form as the EditItemAction, with the same
special form values.
'''
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+
# parse the props from the form
try:
props, links = self.client.parsePropsFromForm(create=1)
except (ValueError, KeyError), message:
- self.client.error_message.append(_('Error: ') + str(message))
+ self.client.error_message.append(self._('Error: %s')
+ % str(message))
return
# handle the props - edit or create
try:
# when it hits the None element, it'll set self.nodeid
messages = self._editnodes(props, links)
-
- except (ValueError, KeyError, IndexError), message:
+ except (ValueError, KeyError, IndexError,
+ roundup.exceptions.Reject), message:
# these errors might just be indicative of user dumbness
- self.client.error_message.append(_('Error: ') + str(message))
+ self.client.error_message.append(_('Error: %s') % str(message))
return
# commit now that all the tricky stuff is done
self.db.commit()
# redirect to the new item's page
- raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
- self.classname, self.nodeid, urllib.quote(messages),
+ raise exceptions.Redirect, '%s%s%s?@ok_message=%s&@template=%s' % (
+ self.base, self.classname, self.nodeid, urllib.quote(messages),
urllib.quote(self.template))
class PassResetAction(Action):
"otk" performs the reset.
"""
+ otks = self.db.getOTKManager()
if self.form.has_key('otk'):
# pull the rego information out of the otk database
otk = self.form['otk'].value
- otks = self.db.getOTKManager()
- uid = otks.get(otk, 'uid')
+ uid = otks.get(otk, 'uid', default=None)
if uid is None:
- self.client.error_message.append("""Invalid One Time Key!
-(a Mozilla bug may cause this message to show up erroneously,
- please check your email)""")
+ self.client.error_message.append(
+ self._("Invalid One Time Key!\n"
+ "(a Mozilla bug may cause this message "
+ "to show up erroneously, please check your email)"))
return
# re-open the database as "admin"
if self.user != 'admin':
self.client.opendb('admin')
self.db = self.client.db
+ otks = self.db.getOTKManager()
# change the password
newpw = password.generatePassword()
return
self.client.ok_message.append(
- 'Password reset and email sent to %s'%address)
+ self._('Password reset and email sent to %s') % address)
return
# no OTK, so now figure the user
try:
uid = self.db.user.lookup(name)
except KeyError:
- self.client.error_message.append('Unknown username')
+ self.client.error_message.append(self._('Unknown username'))
return
address = self.db.user.get(uid, 'address')
elif self.form.has_key('address'):
address = self.form['address'].value
uid = uidFromAddress(self.db, ('', address), create=0)
if not uid:
- self.client.error_message.append('Unknown email address')
+ self.client.error_message.append(
+ self._('Unknown email address'))
return
name = self.db.user.get(uid, 'username')
else:
- self.client.error_message.append('You need to specify a username '
- 'or address')
+ self.client.error_message.append(
+ self._('You need to specify a username or address'))
return
# generate the one-time-key and store the props for later
if not self.client.standard_message([address], subject, body):
return
- self.client.ok_message.append('Email sent to %s'%address)
-
-class ConfRegoAction(Action):
- def handle(self):
- """Grab the OTK, use it to load up the new user details."""
- try:
- # pull the rego information out of the otk database
- self.userid = self.db.confirm_registration(self.form['otk'].value)
- except (ValueError, KeyError), message:
- self.client.error_message.append(str(message))
- return
+ self.client.ok_message.append(self._('Email sent to %s') % address)
+class RegoCommon(Action):
+ def finishRego(self):
# log the new user in
- self.client.user = self.db.user.get(self.userid, 'username')
+ self.client.userid = self.userid
+ user = self.client.user = self.db.user.get(self.userid, 'username')
# re-open the database for real, using the user
- self.client.opendb(self.client.user)
+ self.client.opendb(user)
- # if we have a session, update it
- if hasattr(self, 'session'):
- self.client.db.sessions.set(self.session, user=self.user,
- last_use=time.time())
- else:
- # new session cookie
- self.client.set_cookie(self.user)
+ # update session data
+ self.client.session_api.set(user=user)
# nice message
- message = _('You are now registered, welcome!')
+ message = self._('You are now registered, welcome!')
url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
urllib.quote(message))
window.setTimeout('window.location = "%s"', 1000);
</script>'''%(message, url, message, url)
-class RegisterAction(Action):
+class ConfRegoAction(RegoCommon):
+ def handle(self):
+ """Grab the OTK, use it to load up the new user details."""
+ try:
+ # pull the rego information out of the otk database
+ self.userid = self.db.confirm_registration(self.form['otk'].value)
+ except (ValueError, KeyError), message:
+ self.client.error_message.append(str(message))
+ return
+ return self.finishRego()
+
+class RegisterAction(RegoCommon, EditCommon):
name = 'register'
- permissionType = 'Web Registration'
+ permissionType = 'Register'
def handle(self):
"""Attempt to create a new user based on the contents of the form
- and then set the cookie.
+ and then remember it in session.
Return 1 on successful login.
"""
- props = self.client.parsePropsFromForm(create=1)[0][('user', None)]
-
- # registration isn't allowed to supply roles
- if props.has_key('roles'):
- raise Unauthorised, _("It is not permitted to supply roles "
- "at registration.")
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
- username = props['username']
+ # parse the props from the form
try:
- self.db.user.lookup(username)
- self.client.error_message.append(_('Error: A user with the '
- 'username "%(username)s" already exists')%props)
+ props, links = self.client.parsePropsFromForm(create=1)
+ except (ValueError, KeyError), message:
+ self.client.error_message.append(self._('Error: %s')
+ % str(message))
return
- except KeyError:
- pass
+
+ # registration isn't allowed to supply roles
+ user_props = props[('user', None)]
+ if user_props.has_key('roles'):
+ raise exceptions.Unauthorised, self._(
+ "It is not permitted to supply roles at registration.")
+
+ # skip the confirmation step?
+ if self.db.config['INSTANT_REGISTRATION']:
+ # handle the create now
+ try:
+ # when it hits the None element, it'll set self.nodeid
+ messages = self._editnodes(props, links)
+ except (ValueError, KeyError, IndexError,
+ roundup.exceptions.Reject), message:
+ # these errors might just be indicative of user dumbness
+ self.client.error_message.append(_('Error: %s') % str(message))
+ return
+
+ # fix up the initial roles
+ self.db.user.set(self.nodeid,
+ roles=self.db.config['NEW_WEB_USER_ROLES'])
+
+ # commit now that all the tricky stuff is done
+ self.db.commit()
+
+ # finish off by logging the user in
+ self.userid = self.nodeid
+ return self.finishRego()
# generate the one-time-key and store the props for later
for propname, proptype in self.db.user.getprops().items():
- value = props.get(propname, None)
+ value = user_props.get(propname, None)
if value is None:
pass
elif isinstance(proptype, hyperdb.Date):
- props[propname] = str(value)
+ user_props[propname] = str(value)
elif isinstance(proptype, hyperdb.Interval):
- props[propname] = str(value)
+ user_props[propname] = str(value)
elif isinstance(proptype, hyperdb.Password):
- props[propname] = str(value)
+ user_props[propname] = str(value)
otks = self.db.getOTKManager()
+ otk = ''.join([random.choice(chars) for x in range(32)])
while otks.exists(otk):
otk = ''.join([random.choice(chars) for x in range(32)])
- otks.set(otk, **props)
+ otks.set(otk, **user_props)
# send the email
tracker_name = self.db.config.TRACKER_NAME
tracker_email = self.db.config.TRACKER_EMAIL
- subject = 'Complete your registration to %s -- key %s'%(tracker_name,
+ if self.db.config['EMAIL_REGISTRATION_CONFIRMATION']:
+ subject = 'Complete your registration to %s -- key %s'%(tracker_name,
otk)
- body = """To complete your registration of the user "%(name)s" with
+ body = """To complete your registration of the user "%(name)s" with
%(tracker)s, please do one of the following:
- send a reply to %(tracker_email)s and maintain the subject line as is (the
%(url)s?@action=confrego&otk=%(otk)s
-""" % {'name': props['username'], 'tracker': tracker_name, 'url': self.base,
- 'otk': otk, 'tracker_email': tracker_email}
- if not self.client.standard_message([props['address']], subject, body,
- tracker_email):
+""" % {'name': user_props['username'], 'tracker': tracker_name,
+ 'url': self.base, 'otk': otk, 'tracker_email': tracker_email}
+ else:
+ subject = 'Complete your registration to %s'%(tracker_name)
+ body = """To complete your registration of the user "%(name)s" with
+%(tracker)s, please visit the following URL:
+
+%(url)s?@action=confrego&otk=%(otk)s
+
+""" % {'name': user_props['username'], 'tracker': tracker_name,
+ 'url': self.base, 'otk': otk}
+ if not self.client.standard_message([user_props['address']], subject,
+ body, (tracker_name, tracker_email)):
return
# commit changes to the database
self.db.commit()
# redirect to the "you're almost there" page
- raise Redirect, '%suser?@template=rego_progress'%self.base
+ raise exceptions.Redirect, '%suser?@template=rego_progress'%self.base
class LogoutAction(Action):
def handle(self):
- """Make us really anonymous - nuke the cookie too."""
+ """Make us really anonymous - nuke the session too."""
# log us out
self.client.make_user_anonymous()
-
- # construct the logout cookie
- now = Cookie._getdate()
- self.client.additional_headers['Set-Cookie'] = \
- '%s=deleted; Max-Age=0; expires=%s; Path=%s;'%(self.client.cookie_name,
- now, self.client.cookie_path)
+ self.client.session_api.destroy()
# Let the user know what's going on
- self.client.ok_message.append(_('You are logged out'))
+ self.client.ok_message.append(self._('You are logged out'))
+
+ # reset client context to render tracker home page
+ # instead of last viewed page (may be inaccessibe for anonymous)
+ self.client.classname = None
+ self.client.nodeid = None
+ self.client.template = None
class LoginAction(Action):
def handle(self):
Sets up a session for the user which contains the login credentials.
"""
+ # ensure modification comes via POST
+ if self.client.env['REQUEST_METHOD'] != 'POST':
+ raise roundup.exceptions.Reject(self._('Invalid request'))
+
# we need the username at a minimum
if not self.form.has_key('__login_name'):
- self.client.error_message.append(_('Username required'))
+ self.client.error_message.append(self._('Username required'))
return
# get the login info
else:
password = ''
- # make sure the user exists
try:
- self.client.userid = self.db.user.lookup(self.client.user)
- except KeyError:
- name = self.client.user
- self.client.error_message.append(_('No such user "%(name)s"')%locals())
+ self.verifyLogin(self.client.user, password)
+ except exceptions.LoginError, err:
self.client.make_user_anonymous()
+ self.client.error_message.extend(list(err.args))
return
+ # now we're OK, re-open the database for real, using the user
+ self.client.opendb(self.client.user)
+
+ # save user in session
+ self.client.session_api.set(user=self.client.user)
+ if self.form.has_key('remember'):
+ self.client.session_api.update(set_cookie=True, expire=24*3600*365)
+
+ # If we came from someplace, go back there
+ if self.form.has_key('__came_from'):
+ raise exceptions.Redirect, self.form['__came_from'].value
+
+ def verifyLogin(self, username, password):
+ # make sure the user exists
+ try:
+ self.client.userid = self.db.user.lookup(username)
+ except KeyError:
+ raise exceptions.LoginError, self._('Invalid login')
+
# verify the password
if not self.verifyPassword(self.client.userid, password):
- self.client.make_user_anonymous()
- self.client.error_message.append(_('Incorrect password'))
- return
+ raise exceptions.LoginError, self._('Invalid login')
# Determine whether the user has permission to log in.
# Base behaviour is to check the user has "Web Access".
if not self.hasPermission("Web Access"):
- self.client.make_user_anonymous()
- self.client.error_message.append(_("You do not have permission to login"))
- return
-
- # now we're OK, re-open the database for real, using the user
- self.client.opendb(self.client.user)
-
- # set the session cookie
- self.client.set_cookie(self.client.user)
+ raise exceptions.LoginError, self._(
+ "You do not have permission to login")
def verifyPassword(self, userid, password):
- ''' Verify the password that the user has supplied
- '''
- stored = self.db.user.get(self.client.userid, 'password')
+ '''Verify the password that the user has supplied'''
+ stored = self.db.user.get(userid, 'password')
if password == stored:
return 1
if not password and not stored:
return 1
return 0
+
+class ExportCSVAction(Action):
+ name = 'export'
+ permissionType = 'View'
+
+ def handle(self):
+ ''' Export the specified search query as CSV. '''
+ # figure the request
+ request = templating.HTMLRequest(self.client)
+ filterspec = request.filterspec
+ sort = request.sort
+ group = request.group
+ columns = request.columns
+ klass = self.db.getclass(request.classname)
+
+ # full-text search
+ if request.search_text:
+ matches = self.db.indexer.search(
+ re.findall(r'\b\w{2,25}\b', request.search_text), klass)
+ else:
+ matches = None
+
+ h = self.client.additional_headers
+ h['Content-Type'] = 'text/csv; charset=%s' % self.client.charset
+ # some browsers will honor the filename here...
+ h['Content-Disposition'] = 'inline; filename=query.csv'
+
+ self.client.header()
+
+ if self.client.env['REQUEST_METHOD'] == 'HEAD':
+ # all done, return a dummy string
+ return 'dummy'
+
+ wfile = self.client.request.wfile
+ if self.client.charset != self.client.STORAGE_CHARSET:
+ wfile = codecs.EncodedFile(wfile,
+ self.client.STORAGE_CHARSET, self.client.charset, 'replace')
+
+ writer = csv.writer(wfile)
+ self.client._socket_op(writer.writerow, columns)
+
+ # and search
+ for itemid in klass.filter(matches, filterspec, sort, group):
+ row = []
+ for name in columns:
+ # check permission to view this property on this item
+ if not self.hasPermission('View', itemid=itemid,
+ classname=request.classname, property=name):
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to view %(class)s'
+ ) % {'class': request.classname}
+ row.append(str(klass.get(itemid, name)))
+ self.client._socket_op(writer.writerow, row)
+
+ return '\n'
+
+
+class Bridge(BaseAction):
+ """Make roundup.actions.Action executable via CGI request.
+
+ Using this allows users to write actions executable from multiple frontends.
+ CGI Form content is translated into a dictionary, which then is passed as
+ argument to 'handle()'. XMLRPC requests have to pass this dictionary
+ directly.
+ """
+
+ def __init__(self, *args):
+
+ # As this constructor is callable from multiple frontends, each with
+ # different Action interfaces, we have to look at the arguments to
+ # figure out how to complete construction.
+ if (len(args) == 1 and
+ hasattr(args[0], '__class__') and
+ args[0].__class__.__name__ == 'Client'):
+ self.cgi = True
+ self.execute = self.execute_cgi
+ self.client = args[0]
+ self.form = self.client.form
+ else:
+ self.cgi = False
+
+ def execute_cgi(self):
+ args = {}
+ for key in self.form.keys():
+ args[key] = self.form.getvalue(key)
+ self.permission(args)
+ return self.handle(args)
+
+ def permission(self, args):
+ """Raise Unauthorised if the current user is not allowed to execute
+ this action. Users may override this method."""
+
+ pass
+
+ def handle(self, args):
+
+ raise NotImplementedError
+
+# vim: set filetype=python sts=4 sw=4 et si :