summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 695e038)
raw | patch | inline | side by side (parent: 695e038)
author | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Sun, 1 Sep 2002 12:18:41 +0000 (12:18 +0000) | ||
committer | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Sun, 1 Sep 2002 12:18:41 +0000 (12:18 +0000) |
back out if someone comes up with a better idea) so editing "my details"
works again. Rationalised and cleaned up the actions in any case.
. fixed some more display issues (stuff appearing when it should and shouldn't)
. trying a nicer colouring scheme for the top level page
. handle no grouping being specified
. fixed journaltag so the logged-in user is journalled, not admin!
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1020 57a73879-2fb5-44c3-a270-3262357dd7e2
works again. Rationalised and cleaned up the actions in any case.
. fixed some more display issues (stuff appearing when it should and shouldn't)
. trying a nicer colouring scheme for the top level page
. handle no grouping being specified
. fixed journaltag so the logged-in user is journalled, not admin!
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1020 57a73879-2fb5-44c3-a270-3262357dd7e2
diff --git a/TODO.txt b/TODO.txt
index 4e48e0ec77167889d3d62c072a104bfe666e10a4..30cad613f19656f42f3e74cfa10be77d178c6c4f 100644 (file)
--- a/TODO.txt
+++ b/TODO.txt
. query saving
. search "refinement" (pre-fill the search page with the current search
parameters)
-. security on actions (only allows/enforces generic Edit perm on the class :()
+. web registration of new users by anonymous
ongoing: any bugs
diff --git a/doc/customizing.txt b/doc/customizing.txt
index d2e466b5a2e6d41c24fc750b412bf8cafc62c0d1..7957eec2b44b80b407c8c6d889b1fc5236d7680d 100644 (file)
--- a/doc/customizing.txt
+++ b/doc/customizing.txt
Customising Roundup
===================
-:Version: $Revision: 1.15 $
+:Version: $Revision: 1.16 $
.. contents::
- Web Registration
- Web Access
+- Web Roles
- Email Registration
- Email Access
These are hooked into the default Roles:
-- Admin (Edit everything, View everything)
+- Admin (Edit everything, View everything, Web Roles)
- User (Web Access, Email Access)
- Anonymous (Web Registration, Email Registration)
You may use the ``roundup-admin`` "``security``" command to display the
current Role and Permission configuration in your instance.
+Adding a new Permission
+~~~~~~~~~~~~~~~~~~~~~~~
+
+When adding a new Permission, you will need to:
+
+1. add it to your instance's dbinit so it is created
+2. enable it for the Roles that should have it (verify with
+ "``roundup-admin security``")
+3. add it to the relevant HTML interface templates
+4. add it to the appropriate xxxPermission methods on in your instance
+ interfaces module
+
+
-----------------
diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
index 81695009a719c6a80a4aa59eb6077e4a4d51882d..3c2a87e062a15f94453dc5893601f8f796e26b15 100644 (file)
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
-# $Id: client.py,v 1.2 2002-09-01 04:32:30 richard Exp $
+# $Id: client.py,v 1.3 2002-09-01 12:18:40 richard Exp $
__doc__ = """
WWW request handler (also used in the stand-alone server).
else:
self.user = user
+ # reopen the database as the correct user
+ self.opendb(self.user)
+
def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
''' Determine the context of this page:
# these are the actions that are available
actions = {
- 'edit': 'edititem_action',
- 'new': 'newitem_action',
+ 'edit': 'editItemAction',
+ 'new': 'newItemAction',
'login': 'login_action',
'logout': 'logout_action',
'register': 'register_action',
- 'search': 'search_action',
+ 'search': 'searchAction',
}
def handle_action(self):
''' Determine whether there should be an _action called.
The action is defined by the form variable :action which
identifies the method on this object to call. The four basic
actions are defined in the "actions" dictionary on this class:
- "edit" -> self.edititem_action
- "new" -> self.newitem_action
+ "edit" -> self.editItemAction
+ "new" -> self.newItemAction
"login" -> self.login_action
"logout" -> self.logout_action
"register" -> self.register_action
- "search" -> self.search_action
+ "search" -> self.searchAction
'''
if not self.form.has_key(':action'):
path = '/'.join((self.env['SCRIPT_NAME'], self.env['INSTANCE_NAME'],
''))
self.header(headers={'Set-Cookie':
- 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
-# 'Location': self.db.config.DEFAULT_VIEW}, response=301)
+ 'roundup_user=deleted; Max-Age=0; expires=%s; Path=%s;'%(now, path)})
- # suboptimal, but will do for now
+ # Let the user know what's going on
self.ok_message.append(_('You are logged out'))
- #raise Redirect, None
def register_action(self):
'''Attempt to create a new user based on the contents of the form
# nice message
self.ok_message.append(_('You are now registered, welcome!'))
- def edititem_action(self):
+ def editItemAction(self):
''' Perform an edit of an item in the database.
Some special form elements:
'''
cl = self.db.classes[self.classname]
+ # parse the props from the form
+ try:
+ props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
+ except (ValueError, KeyError), message:
+ self.error_message.append(_('Error: ') + str(message))
+ return
+
# check permission
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Edit', userid, self.classname):
+ if not self.editItemPermission(props):
self.error_message.append(
- _('You do not have permission to edit %(classname)s' %
+ _('You do not have permission to edit %(classname)s'%
self.__dict__))
return
# perform the edit
try:
- props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
-
# make changes to the node
props = self._changenode(props)
-
# handle linked nodes
self._post_editnode(self.nodeid)
-
except (ValueError, KeyError), message:
self.error_message.append(_('Error: ') + str(message))
return
raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
self.nodeid, urllib.quote(message))
- def newitem_action(self):
+ 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:
+ return 1
+ if not self.db.security.hasPermission('Edit', self.userid,
+ self.classname):
+ return 0
+ return 1
+
+ def newItemAction(self):
''' Add a new item to the database.
- This follows the same form as the edititem_action
+ This follows the same form as the editItemAction
'''
- # check permission
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Edit', userid, self.classname):
+ cl = self.db.classes[self.classname]
+
+ # parse the props from the form
+ try:
+ props = parsePropsFromForm(self.db, cl, self.form, self.nodeid)
+ except (ValueError, KeyError), message:
+ self.error_message.append(_('Error: ') + str(message))
+ return
+
+ if not self.newItemPermission(props):
self.error_message.append(
_('You do not have permission to create %s' %self.classname))
try:
# do the create
- nid = self._createnode()
+ nid = self._createnode(props)
# handle linked nodes
self._post_editnode(nid)
raise Redirect, '%s/%s%s?:ok_message=%s'%(self.base, self.classname,
nid, urllib.quote(message))
- def genericedit_action(self):
+ 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 not has('Edit', self.userid, self.classname):
+ return 0
+ return 1
+
+ def genericEditAction(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.
'''
- userid = self.db.user.lookup(self.user)
- if not self.db.security.hasPermission('Edit', userid, self.classname):
- raise Unauthorised, _("You do not have permission to access"\
- " %(action)s.")%{'action': self.classname}
- cl = self.db.classes[self.classname]
- idlessprops = cl.getprops(protected=0).keys()
- props = ['id'] + idlessprops
+ # generic edit is per-class only
+ if not self.genericEditPermission():
+ self.error_message.append(
+ _('You do not have permission to edit %s' %self.classname))
# get the CSV module
try:
'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
return
+ cl = self.db.classes[self.classname]
+ idlessprops = cl.getprops(protected=0).keys()
+ props = ['id'] + idlessprops
+
# do the edit
rows = self.form['rows'].value.splitlines()
p = csv.parser()
raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname,
urllib.quote(message))
+ def genericEditPermission(self):
+ ''' Determine whether the user has permission to edit this class.
+
+ Base behaviour is to check the user can edit this class.
+ '''
+ if not self.db.security.hasPermission('Edit', self.userid,
+ self.classname):
+ return 0
+ return 1
+
+ def searchAction(self):
+ ''' Mangle some of the form variables.
+
+ Set the form ":filter" variable based on the values of the
+ filter variables - if they're set to anything other than
+ "dontcare" then add them to :filter.
+ '''
+ # generic edit is per-class only
+ if not self.searchPermission():
+ self.error_message.append(
+ _('You do not have permission to search %s' %self.classname))
+
+ # add a faked :filter form variable for each filtering prop
+ props = self.db.classes[self.classname].getprops()
+ for key in self.form.keys():
+ if not props.has_key(key): continue
+ if not self.form[key].value: continue
+ self.form.value.append(cgi.MiniFieldStorage(':filter', key))
+
+ def searchPermission(self):
+ ''' Determine whether the user has permission to search this class.
+
+ Base behaviour is to check the user can view this class.
+ '''
+ if not self.db.security.hasPermission('View', self.userid,
+ self.classname):
+ return 0
+ return 1
+
+ def XXXremove_action(self, dre=re.compile(r'([^\d]+)(\d+)')):
+ # XXX I believe this could be handled by a regular edit action that
+ # just sets the multilink...
+ # XXX handle this !
+ target = self.index_arg(':target')[0]
+ m = dre.match(target)
+ if m:
+ classname = m.group(1)
+ nodeid = m.group(2)
+ cl = self.db.getclass(classname)
+ cl.retire(nodeid)
+ # now take care of the reference
+ parentref = self.index_arg(':multilink')[0]
+ parent, prop = parentref.split(':')
+ m = dre.match(parent)
+ if m:
+ self.classname = m.group(1)
+ self.nodeid = m.group(2)
+ cl = self.db.getclass(self.classname)
+ value = cl.get(self.nodeid, prop)
+ value.remove(nodeid)
+ cl.set(self.nodeid, **{prop:value})
+ func = getattr(self, 'show%s'%self.classname)
+ return func()
+ else:
+ raise NotFound, parent
+ else:
+ raise NotFound, target
+
+ #
+ # Utility methods for editing
+ #
def _changenode(self, props):
''' change the node based on the contents of the form
'''
# make the changes
return cl.set(self.nodeid, **props)
- def _createnode(self):
+ def _createnode(self, props):
''' create a node based on the contents of the form
'''
cl = self.db.classes[self.classname]
- props = parsePropsFromForm(self.db, cl, self.form)
# check for messages and files
message, files = self._handle_message()
link = self.db.classes[link]
link.set(nodeid, **{property: nid})
- def search_action(self):
- ''' Mangle some of the form variables.
-
- Set the form ":filter" variable based on the values of the
- filter variables - if they're set to anything other than
- "dontcare" then add them to :filter.
- '''
- # add a faked :filter form variable for each filtering prop
- props = self.db.classes[self.classname].getprops()
- for key in self.form.keys():
- if not props.has_key(key): continue
- if not self.form[key].value: continue
- self.form.value.append(cgi.MiniFieldStorage(':filter', key))
-
- def remove_action(self, dre=re.compile(r'([^\d]+)(\d+)')):
- # XXX handle this !
- target = self.index_arg(':target')[0]
- m = dre.match(target)
- if m:
- classname = m.group(1)
- nodeid = m.group(2)
- cl = self.db.getclass(classname)
- cl.retire(nodeid)
- # now take care of the reference
- parentref = self.index_arg(':multilink')[0]
- parent, prop = parentref.split(':')
- m = dre.match(parent)
- if m:
- self.classname = m.group(1)
- self.nodeid = m.group(2)
- cl = self.db.getclass(self.classname)
- value = cl.get(self.nodeid, prop)
- value.remove(nodeid)
- cl.set(self.nodeid, **{prop:value})
- func = getattr(self, 'show%s'%self.classname)
- return func()
- else:
- raise NotFound, parent
- else:
- raise NotFound, target
-
def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
'''Pull properties for the given class out of the form.
index 2a04f5ab44ea1eba69522dccd351f10bd4587ae7..93051a0605226f7b29f59b178ccb236933c70703 100644 (file)
if self.form.has_key(':filter'):
self.filter = handleListCGIValue(self.form[':filter'])
self.filterspec = {}
- props = self.client.db.getclass(self.classname).getprops()
- for name in self.filter:
- if self.form.has_key(name):
- prop = props[name]
- if (isinstance(prop, hyperdb.Link) or
- isinstance(prop, hyperdb.Multilink)):
- self.filterspec[name] = handleListCGIValue(self.form[name])
- else:
- self.filterspec[name] = self.form[name].value
+ if self.classname is not None:
+ props = self.client.db.getclass(self.classname).getprops()
+ for name in self.filter:
+ if self.form.has_key(name):
+ prop = props[name]
+ fv = self.form[name]
+ if (isinstance(prop, hyperdb.Link) or
+ isinstance(prop, hyperdb.Multilink)):
+ self.filterspec[name] = handleListCGIValue(fv)
+ else:
+ self.filterspec[name] = fv.value
# full-text search argument
self.search_text = None
if columns and self.columns:
l.append(s%(':columns', ','.join(self.columns.keys())))
if sort and self.sort is not None:
- l.append(s%(':sort', self.sort))
+ if self.sort[0] == '-':
+ val = '-'+self.sort[1]
+ else:
+ val = self.sort[1]
+ l.append(s%(':sort', val))
if group and self.group is not None:
- l.append(s%(':group', self.group))
+ if self.group[0] == '-':
+ val = '-'+self.group[1]
+ else:
+ val = self.group[1]
+ l.append(s%(':group', val))
if filter and self.filter:
l.append(s%(':filter', ','.join(self.filter)))
if filterspec:
if self.columns:
l.append(':columns=%s'%(','.join(self.columns.keys())))
if self.sort is not None:
- l.append(':sort=%s'%self.sort)
+ if self.sort[0] == '-':
+ val = '-'+self.sort[1]
+ else:
+ val = self.sort[1]
+ l.append(':sort=%s'%val)
if self.group is not None:
- l.append(':group=%s'%self.group)
+ if self.group[0] == '-':
+ val = '-'+self.group[1]
+ else:
+ val = self.group[1]
+ l.append(':group=%s'%val)
if self.filter:
l.append(':filter=%s'%(','.join(self.filter)))
for k,v in self.filterspec.items():
diff --git a/roundup/templates/classic/html/issue.index b/roundup/templates/classic/html/issue.index
index 13b316fbb6377ae919e563dad4fb1a398bfeae06..fc47be7ef458aa6728cec87bf018f38e7e90cfa2 100644 (file)
<th tal:condition="exists:request/columns/assignedto">Assigned To</th>
</tr>
<tal:block tal:repeat="i batch">
- <tr tal:condition="python:batch.propchanged(request.group[1])">
+ <tr tal:condition="python:request.group[1] and
+ batch.propchanged(request.group[1])">
<th tal:attributes="colspan python:len(request.columns)"
tal:content="python:i[request.group[1]]" class="group">
</th>
<tr>
<td style="padding: 0" tal:attributes="colspan python:len(request.columns)">
<table class="list">
- <tr><th style="text-align: left">
+ <tr><th style="text-align: left; border: 0">
<a tal:define="prev batch/previous" tal:condition="prev"
tal:attributes="href python:request.indexargs_href(request.classname,
{':startwith':prev.start, ':pagesize':prev.size})"><< previous</a>
</th>
- <th style="text-align: right">
+ <th style="text-align: right; border: 0">
<a tal:define="next batch/next" tal:condition="next"
tal:attributes="href python:request.indexargs_href(request.classname,
{':startwith':next.start, ':pagesize':next.size})">next >></a>
index c6d6f98fb5e45786ea7a2cdaa4e8f1196c296c74..5194258473e212bd71e905a4d9c43f1b7e26b4b5 100644 (file)
<tr>
<td rowspan="2" valign="top" nowrap class="sidebar">
<p class="classblock"
- tal:condition="python:request.user.hasPermission('Edit', 'issue')">
+ tal:condition="python:request.user.hasPermission('View', 'issue')">
<b>Issues</b><br>
+ <a tal:condition="python:request.user.hasPermission('Edit', 'issue')"
+ href="issue?:template=item">New Issue<br></a>
<a href="issue?:sort=-activity&:group=priority&:filter=status,assignedto&:columns=id,activity,title,creator,priority&status=-1,1,2,3,4,5,6,7&assignedto=-1">Unassigned Issues</a><br>
<a href="issue?:sort=-activity&:group=priority&:filter=status&:columns=id,activity,title,creator,assignedto,priority&status=-1,1,2,3,4,5,6,7">All Issues</a><br>
- <a href="issue?:template=search">Search Issues</a><br>
- <a href="issue?:template=item">New Issue</a>
+ <a href="issue?:template=search">Search Issues</a>
</p>
<p class="classblock"
</p>
<p class="userblock">
- <b>Logged in as</b><br><b tal:content="request/user/username">username</b><br>
+ <b>Hello,</b><br><b tal:content="request/user/username">username</b><br>
<form method="POST" action=''
tal:condition="python:request.user.username=='anonymous'">
<input size="10" name="__login_name"><br>
index c8bccc06b59eca6babcd7628b3682f80f94c201a..a3004278cdaf120b106fd657d6e2ae5b6b8e307f 100644 (file)
a { text-decoration: none; }
.page-header-left {
- background-color: #ffffee;
+ background-color: #cccc88;
padding: 5px;
}
.page-header-top {
- background-color: #ffffee;
- border-bottom: 1px solid #ffffbb;
+ background-color: #cccc88;
+ border-bottom: 1px solid #dddd99;
padding: 5px;
}
td.sidebar {
- background-color: #ffffee;
- border-right: 1px solid #ffffbb;
- border-bottom: 1px solid #ffffbb;
- padding: 5px;
+ background-color: #cccc88;
+ border-right: 1px solid #dddd99;
+ border-bottom: 1px solid #dddd99;
+ padding: 0px;
}
td.sidebar p.classblock {
- border-top: 1px solid #ffffbb;
- border-bottom: 1px solid #ffffbb;
+ padding: 0 5 0 5;
+ border-top: 1px solid #dddd99;
+ border-bottom: 1px solid #dddd99;
}
td.sidebar p.userblock {
- background-color: #eeffff;
- border-top: 1px solid #bbffff;
- border-bottom: 1px solid #bbffff;
+ padding: 0 5 0 5;
+ background-color: #dddd99;
+ border-top: 1px solid #ffffbb;
+ border-bottom: 1px solid #ffffbb;
}
td.content {
table.form {
border-spacing: 0px;
border-collapse: separate;
- /* width: 100%; */
}
.form th {
padding: 0 4 0 4;
color: #404070;
background-color: #eeeeff;
-/*
border-right: 1px solid #404070;
-*/
vertical-align: top;
}
-table.list th a:hover { color: white }
-table.list th a:link { color: white }
-table.list th a { color: white }
+table.list th a:hover { color: #404070 }
+table.list th a:link { color: #404070 }
+table.list th a { color: #404070 }
table.list th.group {
text-align: center;
}
border-left: 1px solid #404070;
border-right: 1px solid #404070;
}
-/*
+
table.list th:first-child {
border-left: 1px solid #404070;
border-right: 1px solid #404070;
}
-*/
+
/* style for message displays */
table.messages {
index f44c23392cce2763a32e48be9096df3ee7196771..28fff4485af5f281ff1fc3f4379ea72e2a46330a 100644 (file)
</tr>
<tr tal:condition="python:request.user.hasPermission('Web Roles')">
<th>Roles</th>
- <td tal:content="structure user/roles/field">roles</td>
+ <td tal:condition="exists:item"
+ tal:content="structure user/roles/field">roles</td>
+ <td tal:condition="not:exists:item">
+ <input name="roles" tal:attributes="value db/config/NEW_WEB_USER_ROLES">
+ </td>
</tr>
<tr>
<th>Phone</th>
</table>
</form>
-<table class="otherinfo" tal:condition="user/queries">
- <tr><th class="header">Queries</th></tr>
- <tr tal:repeat="query user/queries">
- <td tal:content="query">query</td>
- </tr>
-</table>
+<tal:block tal:condition="exists:item">
+ <table class="otherinfo" tal:condition="user/queries">
+ <tr><th class="header">Queries</th></tr>
+ <tr tal:repeat="query user/queries">
+ <td tal:content="query">query</td>
+ </tr>
+ </table>
+
+ <table class="otherinfo">
+ <tr><th class="header">History</th></tr>
+ <tr>
+ <td tal:content="structure user/history">history</td>
+ </tr>
+ </table>
+</tal:block>
-<table class="otherinfo">
- <tr><th class="header">History</th></tr>
- <tr>
- <td tal:content="structure user/history">history</td>
- </tr>
-</table>
</tal:block>
<table class="form" tal:condition="python:viewok and not editok">