diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py
index afc6a107424355b92bc719231f47013d242b9e67..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
+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']
+ 'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
+ '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):
- """Execute the action specified by this object."""
+ """Action handler procedure"""
raise NotImplementedError
+ def execute(self):
+ """Execute the action specified by this object."""
+ self.permission()
+ return self.handle()
+
+ name = ''
+ permissionType = None
def permission(self):
"""Check whether the user has permission to execute this action.
- True by default.
+ True by default. If the permissionType attribute is a string containing
+ a simple permission, check whether the user has that permission.
+ Subclasses must also define the name attribute if they define
+ permissionType.
+
+ Despite having this permission, users may still be unauthorised to
+ perform parts of actions. It is up to the subclasses to detect this.
"""
- return 1
+ if (self.permissionType and
+ not self.hasPermission(self.permissionType)):
+ info = {'action': self.name, 'classname': self.classname}
+ raise exceptions.Unauthorised, self._(
+ 'You do not have permission to '
+ '%(action)s the %(classname)s class.')%info
+
+ _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,
+ 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, 'Invalid %s number'%t
- url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n)
- raise Redirect, url
+ raise ValueError, self._('No type specified')
+ if not n:
+ raise exceptions.SeriousError, self._('No ID entered')
+ try:
+ int(n)
+ except ValueError:
+ d = {'input': n, 'classname': t}
+ raise exceptions.SeriousError, self._(
+ '"%(input)s" is not an ID (%(classname)s ID required)')%d
+ url = '%s%s%s'%(self.base, t, n)
+ raise exceptions.Redirect, url
class RetireAction(Action):
+ name = 'retire'
+ permissionType = 'Edit'
+
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
- # generic edit is per-class only
- if not self.permission():
- raise Unauthorised, _('You do not have permission to retire %s' %
- self.classname)
-
# 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})
-
- def permission(self):
- """Determine whether the user has permission to retire this class.
+ self._('%(classname)s %(itemid)s has been retired')%{
+ 'classname': self.classname.capitalize(), 'itemid': itemid})
- Base behaviour is to check the user can edit this class.
- """
- return self.db.security.hasPermission('Edit', self.client.userid,
- self.client.classname)
class SearchAction(Action):
- def handle(self, wcre=re.compile(r'[\s,]+')):
+ name = 'search'
+ permissionType = 'View'
+
+ def handle(self):
"""Mangle some of the form variables.
Set the form ":filter" variable based on the values of the filter
Split any String query values on whitespace and comma.
"""
- # generic edit is per-class only
- if not self.permission():
- raise Unauthorised, _('You do not have permission to search %s' %
- self.classname)
-
self.fakeFilterVars()
- queryname = self.getQueryName()
+ 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')
- queries.append(qid)
- self.db.user.set(self.userid, queries=queries)
+ if qid not in queries:
+ queries.append(qid)
+ self.db.user.set(self.userid, queries=queries)
# commit the query change to the database
self.db.commit()
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(key, v))
-
+
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 permission(self):
- return self.db.security.hasPermission('View', self.client.userid,
- self.client.classname)
+
+ def getQueryName(self):
+ return self.getFromForm('queryname')
class EditCSVAction(Action):
+ name = 'edit'
+ permissionType = 'Edit'
+
def handle(self):
"""Performs an edit of all of a class' items in one go.
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.
-
"""
- # this is per-class only
- if not self.permission():
- self.client.error_message.append(
- _('You do not have permission to edit %s' %self.classname))
- return
-
- # 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'))
-
- def permission(self):
- return self.db.security.hasPermission('Edit', self.client.userid,
- self.client.classname)
-
-class EditItemAction(Action):
- def handle(self):
- """Perform an edit of an item in the database.
-
- See parsePropsFromForm and _editnodes for special variables.
-
- """
- props, links = self.client.parsePropsFromForm()
+ self.client.ok_message.append(self._('Items edited OK'))
- # handle the props
- try:
- message = self._editnodes(props, links)
- except (ValueError, KeyError, IndexError), message:
- self.client.error_message.append(_('Apply Error: ') + str(message))
- return
-
- # commit now that all the tricky stuff is done
- self.db.commit()
+class EditCommon(Action):
+ '''Utility methods for editing.'''
- # redirect to the item's edit page
- # redirect to finish off
- url = self.base + self.classname
- # note that this action might have been called by an index page, so
- # we will want to include index-page args in this URL too
- if self.nodeid is not None:
- url += self.nodeid
- 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
-
- def editItemPermission(self, props):
- """Determine whether the user has permission to edit this item.
-
- 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 this is a user node and the user is editing their own node, then
- # we're OK
- has = self.db.security.hasPermission
- if self.classname == 'user':
- # reject if someone's trying to edit "roles" and doesn't have the
- # right permission.
- if props.has_key('roles') and not has('Web Roles', self.userid,
- 'user'):
- return 0
- # if the item being edited is the current user, we're ok
- if (self.nodeid == self.userid
- and self.db.user.get(self.nodeid, 'username') != 'anonymous'):
- return 1
- if self.db.security.hasPermission('Edit', self.userid, self.classname):
- return 1
- return 0
-
- def newItemAction(self):
- ''' Add a new item to the database.
-
- This follows the same form as the editItemAction, with the same
- special form values.
- '''
- # parse the props from the form
- try:
- props, links = self.parsePropsFromForm(create=True)
- except (ValueError, KeyError), message:
- self.error_message.append(_('Error: ') + 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:
- # these errors might just be indicative of user dumbness
- self.error_message.append(_('Error: ') + 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),
- urllib.quote(self.template))
-
- 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.
-
- """
- has = self.db.security.hasPermission
- if self.classname == 'user' and has('Web Registration', self.userid,
- 'user'):
- return 1
- if has('Edit', self.userid, self.classname):
- 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.client.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)
+
+ 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'):
+ d = date.Date(self.form[':lastactivity'].value)
+ elif self.form.has_key('@lastactivity'):
+ 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)
+ 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, 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.
+
+ """
+ # 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,
+ 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
+ self.db.commit()
+
+ # redirect to the item's edit page
+ # redirect to finish off
+ url = self.base + self.classname
+ # note that this action might have been called by an index page, so
+ # we will want to include index-page args in this URL too
+ if self.nodeid is not None:
+ url += self.nodeid
+ url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
+ urllib.quote(self.template))
+ if self.nodeid is None:
+ req = templating.HTMLRequest(self.client)
+ url += '&' + req.indexargs_url('', {})[1:]
+ raise exceptions.Redirect, url
+
+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(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,
+ roundup.exceptions.Reject), message:
+ # these errors might just be indicative of user dumbness
+ 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 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):
def handle(self):
"""Handle password reset requests.
-
+
Presence of either "name" or "address" generates email. Presence of
"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
- uid = self.db.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()
cl = self.db.user
-# XXX we need to make the "default" page be able to display errors!
+ # XXX we need to make the "default" page be able to display errors!
try:
# set the password
cl.set(uid, password=password.Password(newpw))
# clear the props from the otk database
- self.db.otks.destroy(otk)
+ otks.destroy(otk)
self.db.commit()
except (ValueError, KeyError), message:
self.client.error_message.append(str(message))
if not self.client.standard_message([address], subject, body):
return
- self.client.ok_message.append('Password reset and email sent to %s' %
- address)
+ self.client.ok_message.append(
+ 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
otk = ''.join([random.choice(chars) for x in range(32)])
- self.db.otks.set(otk, uid=uid, __time=time.time())
+ while otks.exists(otk):
+ otk = ''.join([random.choice(chars) for x in range(32)])
+ otks.set(otk, uid=uid)
+ self.db.commit()
# send the email
tracker_name = self.db.config.TRACKER_NAME
if not self.client.standard_message([address], subject, body):
return
- self.client.ok_message.append('Email sent to %s'%address)
+ self.client.ok_message.append(self._('Email sent to %s') % address)
-class ConfRegoAction(Action):
+class RegoCommon(Action):
+ def finishRego(self):
+ # log the new user in
+ 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(user)
+
+ # update session data
+ self.client.session_api.set(user=user)
+
+ # nice message
+ message = self._('You are now registered, welcome!')
+ url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
+ urllib.quote(message))
+
+ # redirect to the user's page (but not 302, as some email clients seem
+ # to want to reload the page, or something)
+ return '''<html><head><title>%s</title></head>
+ <body><p><a href="%s">%s</a></p>
+ <script type="text/javascript">
+ window.setTimeout('window.location = "%s"', 1000);
+ </script>'''%(message, url, message, url)
+
+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:
- # XXX: we need to make the "default" page be able to display errors!
self.client.error_message.append(str(message))
return
-
- # log the new user in
- 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.db = client.db
-
- # if we have a session, update it
- if hasattr(self, 'session'):
- self.db.sessions.set(self.session, user=self.user,
- last_use=time.time())
- else:
- # new session cookie
- self.client.set_cookie(self.user)
-
- # nice message
- message = _('You are now registered, welcome!')
+ return self.finishRego()
- # redirect to the user's page
- raise Redirect, '%suser%s?@ok_message=%s'%(self.base,
- self.userid, urllib.quote(message))
+class RegisterAction(RegoCommon, EditCommon):
+ name = 'register'
+ permissionType = 'Register'
-class RegisterAction(Action):
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()[0][('user', None)]
-
- # make sure we're allowed to register
- if not self.permission(props):
- raise Unauthorised, _("You do not have permission to register")
+ # 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:
- self.db.user.lookup(props['username'])
- self.client.error_message.append('Error: A user with the username "%s" '
- 'already exists'%props['username'])
+ 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
- otk = ''.join([random.choice(chars) for x in range(32)])
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)
- props['__time'] = time.time()
- self.db.otks.set(otk, **props)
+ 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, **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
- or visit the following URL:
%(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
-
- def permission(self, props):
- """Determine whether the user has permission to register
-
- Base behaviour is to check the user has "Web Registration".
-
- """
- # registration isn't allowed to supply roles
- if props.has_key('roles'):
- return 0
- if self.db.security.hasPermission('Web Registration', self.userid):
- return 1
- return 0
+ 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()
- return
-
- # verify the password
- if not self.verifyPassword(self.client.userid, password):
- self.client.make_user_anonymous()
- self.client.error_message.append(_('Incorrect password'))
- return
-
- # make sure we're allowed to be here
- if not self.permission():
- self.client.make_user_anonymous()
- self.client.error_message.append(_("You do not have permission to login"))
+ 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)
- # set the session cookie
- self.client.set_cookie(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):
+ 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"):
+ 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
- def permission(self):
- """Determine whether the user has permission to log in.
+class ExportCSVAction(Action):
+ name = 'export'
+ permissionType = 'View'
- Base behaviour is to check the user has "Web Access".
+ 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
- """
- if not self.db.security.hasPermission('Web Access', self.client.userid):
- return 0
- return 1
+ 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 :