So I wanted a project to familiarize myself with the Trac code base and
I figured #11 (Web configuration/administration instead of trac-admin)
would be a good place to start ;-)
The attached patch is a first stab at exposing relevant functionality
from trac-admin via a new Admin module. The UI's not the greatest (and
not 100% consistent), there's very little error handling and it has a
couple of bugs:
- it only requires TRAC_ADMIN permission, and that's not fully enforced
due to a bug in the permissions code
- version and milestone times are being shifted on each update; probably
the rendering and parsing of times are applying the local timezone
differently, I haven't dug into it
- you can't delete multiple items; it's a UI design limitation for most
stuff, but for priorities, severities and permissions it's because I
haven't figured out a way to access all the values of a multi-valued
HTTP request parameter yet
- alot of code is copy/pasted from trac-admin; I need to go back and
refactor to extract a common, shared module if I carry on with this
So, not something 'ready to commit' but it's a start. I didn't want to
take it any further without some feedback. I achieved my basic goal of
getting some initial familiarity with the code base, so my question is:
should I take this further?
More specifically, Danial/Jonas/Rocky, are you interested in having
someone work on this? Would you place this above or below CVS
integration, which I'm also looking at (as Daniel already knows)?
If you'd be interested in seeing this go forward, I wouldn't mind
advice/suggestions on how the UI could improve, as well as code review
in case I'm doing anything horrible! :-)
Anyway, here it is:
L.
Index: trac/core.py
===================================================================
--- trac/core.py (revision 443)
+++ trac/core.py (working copy)
@@ -51,6 +51,7 @@
'changeset' : ('Changeset', 'Changeset', 1),
'newticket' : ('Ticket', 'Newticket', 0),
'attachment' : ('File', 'Attachment', 0),
+ 'admin' : ('Admin', 'Admin', 0),
}
def parse_path_info(path_info):
@@ -117,6 +118,12 @@
args['id'] = match.group(2)
args['filename'] = match.group(3)
return args
+ match = re.search('^/admin/([a-zA-Z_]+)?/?([a-zA-Z_]+)?', path_info)
+ if match:
+ args['mode'] = 'admin'
+ args['type'] = match.group(1)
+ args['user'] = match.group(2)
+ return args
return args
def parse_args(command, path_info, query_string,
@@ -199,6 +206,7 @@
hdf.setValue('project.name', env.get_config('project', 'name'))
hdf.setValue('project.descr', env.get_config('project', 'descr'))
+ hdf.setValue('trac.href.admin', env.href.admin())
hdf.setValue('trac.href.wiki', env.href.wiki())
hdf.setValue('trac.href.browser', env.href.browser('/'))
hdf.setValue('trac.href.timeline', env.href.timeline())
Index: trac/Admin.py
===================================================================
--- trac/Admin.py (revision 0)
+++ trac/Admin.py (revision 0)
@@ -0,0 +1,346 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2004 Edgewall Software
+# Copyright (C) 2004 Jonas Borgstrˆm <jonas@edgewall.com>
+# Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
+#
+# Trac is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# Trac is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+# Author: Jonas Borgstrˆm <jonas@edgewall.com>
+
+import perm
+from Module import Module
+import neo_cgi
+import neo_cs
+import sqlite
+import time
+
+class Admin (Module):
+ template_name = 'admin.cs'
+
+ def render (self):
+ #self.perm.assert_permission(perm.TRAC_ADMIN) FIXME: fails to
find the permission?!
+ self.req.hdf.setValue('title', 'Admin')
+ type = self.args.get('type', 'components')
+ user = self.args.get('user')
+ if (type == None):
+ type='components'
+ action = self.get_action()
+
+ if action and action != 'view':
+ # Process requested updates
+ # TODO: validate inputs
+ handler = getattr(self, 'do_' + type)
+ handler(action)
+ self.req.redirect(self.env.href.admin(type, user))
+ else:
+ # Render the page
+ self.req.hdf.setValue('admin.type', type)
+ renderer = getattr(self, 'render_' + type)
+ renderer()
+
+ def get_action (self):
+ self.action = 'view'
+ if self.args.get('add'):
+ self.action = 'add'
+ elif self.args.get('update'):
+ self.action = 'update'
+ elif self.args.get('delete'):
+ self.action = 'delete'
+ return self.action
+
+
+ def do_components(self, action):
+ if action == 'add':
+ self.do_component_add()
+ elif action == 'update':
+ self.do_component_update()
+ elif action == 'delete':
+ self.do_component_delete()
+
+ def do_component_add(self):
+ name = self.args.get('name')
+ owner = self.args.get('owner')
+ self._db_execsql("INSERT INTO component VALUES('%s', '%s')"
+ % (name, owner))
+
+ def do_component_update(self):
+ name = self.args.get('name')
+ owner = self.args.get('owner')
+ newname = self.args.get('newname')
+ newowner = self.args.get('newowner')
+
+ if (name != newname) or (owner != newowner):
+ cnx = self.env.get_db_cnx()
+ cursor = cnx.cursor ()
+ if (owner != newowner):
+ self._db_execsql("UPDATE component SET owner='%s' WHERE
name='%s'"
+ % (newowner,name), cursor)
+ if (name != newname):
+ self._db_execsql("UPDATE component SET name='%s' WHERE
name='%s'"
+ % (newname,name), cursor)
+ self._db_execsql("UPDATE ticket SET component='%s'
WHERE component='%s'"
+ % (newname,name), cursor)
+ cnx.commit()
+
+ def do_component_delete(self):
+ name = self.args.get('name')
+ self._db_execsql("DELETE FROM component WHERE name='%s'"
+ % (name))
+
+
+ def do_versions(self, action):
+ if action == 'add':
+ self.do_mile_ver_add('version')
+ if action == 'update':
+ self.do_mile_ver_update('version')
+ if action == 'delete':
+ self.do_mile_ver_delete('version')
+
+ def do_milestones(self, action):
+ if action == 'add':
+ self.do_mile_ver_add('milestone')
+ if action == 'update':
+ self.do_mile_ver_update('milestone')
+ if action == 'delete':
+ self.do_mile_ver_delete('milestone')
+
+ def do_mile_ver_add(self, type):
+ name = self.args.get('name')
+ t = self.args.get('time')
+ seconds = self._parse_time(t)
+
+ self._db_execsql("INSERT INTO %(type)s('name','time') "
+ " VALUES('%(name)s', '%(time)s')"
+ % {'type':type, 'name':name, 'time':seconds})
+
+ def do_mile_ver_update(self, type):
+ name = self.args.get('name')
+ newname = self.args.get('newname')
+ t = self.args.get('newtime')
+ seconds = self._parse_time(t)
+
+ cnx = self.env.get_db_cnx()
+ cursor = cnx.cursor ()
+ self._db_execsql("UPDATE %(type)s SET name='%(newname)s'"
+ " WHERE name='%(name)s'"
+ % {'type':type, 'newname':newname,
'name':name},
+ cursor)
+ if seconds:
+ self._db_execsql("UPDATE %s SET time='%s'"
+ " WHERE name='%s'" % (type, seconds, newname),
+ cursor)
+ cnx.commit()
+
+ def do_mile_ver_delete(self, type):
+ name = self.args.get('name')
+ d = {'name':name, 'type':type}
+ data = self._db_execsql("SELECT name FROM %(type)s"
+ " WHERE name='%(name)s'" % d)
+ #if not data:
+ # raise Exception, "No such %s '%s'" % (type, name) FIXME:
clean error reporting
+ data = self._db_execsql("DELETE FROM %(type)s"
+ " WHERE name='%(name)s'" % d)
+
+
+ def do_priorities(self, action):
+ if action == 'add':
+ self.do_enum_add('priority')
+ if action == 'update':
+ self.do_enum_update('priority')
+ if action == 'delete':
+ self.do_enum_delete('priority')
+
+ def do_severities(self, action):
+ if action == 'add':
+ self.do_enum_add('severity')
+ if action == 'update':
+ self.do_enum_update('severity')
+ if action == 'delete':
+ self.do_enum_delete('severity')
+
+ def do_enum_add(self, type):
+ name = self.args.get('name')
+ sql = ("INSERT INTO enum('value','type','name') "
+ " SELECT 1+ifnull(max(value),0),'%(type)s','%(name)s'"
+ " FROM enum WHERE type='%(type)s'"
+ % {'type':type, 'name':name})
+ self._db_execsql(sql)
+ # FIXME enum already in response; have to do a refresh
+
+ def do_enum_update(self, type):
+ name = self.args.get('selection')
+ newname = self.args.get('name')
+ d = {'name':name, 'newname':newname, 'type':type}
+ data = self._db_execsql("SELECT name FROM enum"
+ " WHERE type='%(type)s' AND
name='%(name)s'" % d)
+ if not data:
+ raise Exception, "No such value '%s'" % name
+ data = self._db_execsql("UPDATE enum SET name='%(newname)s'"
+ " WHERE type='%(type)s' AND
name='%(name)s'" % d)
+
+ def do_enum_delete(self, type):
+ name = self.args.get('selection')
+ data = self._db_execsql("SELECT name FROM enum"
+ " WHERE type='%s' AND name='%s'" %
(type, name))
+ if not data:
+ raise Exception, "No such value '%s'" % name
+ data = self._db_execsql("DELETE FROM enum WHERE type='%s' AND
name='%s'"
+ % (type, name))
+
+
+ def do_permissions(self, action):
+ if action == 'add':
+ self.do_permission_add()
+ if action == 'delete':
+ self.do_permission_remove()
+
+ def do_permission_add(self):
+ user = self.args.get('user')
+ perms = self.args.get('permissions.avail')
+
+ #for action in perms:
+ action = perms
+ self._db_execsql("INSERT INTO permission VALUES('%s', '%s')" %
(user, action))
+
+
+ def do_permission_remove(self):
+ user = self.args.get('user')
+ perms = self.args.get('permissions.grant')
+
+ #for action in perms:
+ action = perms
+ self._db_execsql("DELETE FROM permission WHERE username='%s'
AND action='%s'" %
+ (user, action))
+
+
+ #
+ # Page rendering
+ #
+
+ def render_components (self):
+ idx = 0
+ components = self._db_execsql('SELECT name,owner FROM component
ORDER BY name')
+ for item in components:
+ self.req.hdf.setValue('admin.components.%d.name' % idx,
item[0])
+ self.req.hdf.setValue('admin.components.%d.owner' % idx,
item[1])
+ idx = idx + 1
+
+ def render_versions (self):
+ idx = 0
+ versions = self._db_execsql('SELECT name,time FROM version
ORDER BY time,name')
+ for item in versions:
+ secs = item[1]
+ st = time.gmtime(secs)
+ tm = time.strftime('%x %X', st)
+
+ self.req.hdf.setValue('admin.versions.%d.name' % idx, item[0])
+ self.req.hdf.setValue('admin.versions.%d.time' % idx, tm)
+
+ idx = idx + 1
+
+ def render_milestones (self):
+ idx = 0
+ milestones = self._db_execsql('SELECT name,time FROM milestone
ORDER BY time,name')
+ for item in milestones:
+ secs = item[1]
+ st = time.gmtime(secs)
+ tm = time.strftime('%x %X', st)
+
+ self.req.hdf.setValue('admin.milestones.%d.name' % idx,
item[0])
+ self.req.hdf.setValue('admin.milestones.%d.time' % idx, tm)
+ idx = idx + 1
+ self.req.hdf.setValue('admin.milestone.count', str(idx))
+
+ def render_priorities (self):
+ # data's already present, nothing to do
+ pass
+
+ def render_severities (self):
+ # data's already present, nothing to do
+ pass
+
+ def render_permissions(self):
+ user = self.args.get('user')
+ if user:
+ avail = perm.meta_permission.keys()
+ avail = avail + perm.base_permissions
+
+ data = self._db_execsql("SELECT action FROM permission "
+ "WHERE username='%s'"
+ "ORDER BY action" % user)
+
+ idx = 0
+ for row in data:
+ item = row[0]
+# if item in avail:
+# del avail[item]
+ self.req.hdf.setValue('admin.permissions.grant.%d.name'
% idx, item)
+ idx = idx + 1
+
+ idx = 0
+ avail.sort()
+ for item in avail:
+ self.req.hdf.setValue('admin.permissions.avail.%d.name'
%idx, item)
+ idx = idx + 1
+
+ self.req.hdf.setValue('admin.user', user)
+
+
+ #
+ # Utility methods
+ #
+
+ def _db_open(self):
+ try:
+ #if not self.__env:
+ # self.__env = trac.Environment.Environment (self.envname)
+ #return self.__env.get_db_cnx()
+ return self.env.get_db_cnx()
+ except Exception, e:
+ print 'Failed to open environment.', e
+ sys.exit(1)
+
+ def _db_execsql (self, sql, cursor=None):
+ data = []
+ if not cursor:
+ cnx = self._db_open()
+ cursor = cnx.cursor()
+ else:
+ cnx = None
+ cursor.execute(sql)
+ while 1:
+ row = cursor.fetchone()
+ if row == None:
+ break
+ data.append(row)
+ if cnx:
+ cnx.commit()
+ return data
+
+ def _parse_time(self, t):
+ seconds = None
+ if t == 'now':
+ seconds = str(int(time.time()))
+ else:
+ for format in ['%x %X', '%x, %X', '%X %x', '%X, %x', '%x']:
+ try:
+ seconds = str(time.mktime(time.strptime(t,
format))) #FIXME: time isn't round-tripping correctly
+ except ValueError:
+ continue
+
+ return seconds
+
+
\ No newline at end of file
Index: trac/perm.py
===================================================================
--- trac/perm.py (revision 443)
+++ trac/perm.py (working copy)
@@ -60,7 +60,17 @@
WIKI_ADMIN : [WIKI_VIEW, WIKI_CREATE, WIKI_MODIFY, WIKI_DELETE]
}
-
+base_permissions = [.
+ TIMELINE_VIEW, SEARCH_VIEW, CONFIG_VIEW,
+ LOG_VIEW, FILE_VIEW, CHANGESET_VIEW, BROWSER_VIEW,
+
+ TICKET_VIEW, TICKET_CREATE, TICKET_MODIFY,
+
+ REPORT_VIEW, REPORT_SQL_VIEW, REPORT_CREATE, REPORT_MODIFY,
REPORT_DELETE,
+
+ WIKI_VIEW, WIKI_CREATE, WIKI_MODIFY, WIKI_DELETE
+ ]
+
class PermissionError (StandardError):
"""Insufficient permissions to complete the operation"""
def __init__ (self, action):
Index: trac/Href.py
===================================================================
--- trac/Href.py (revision 443)
+++ trac/Href.py (working copy)
@@ -105,3 +105,10 @@
else:
return href_join(self.base, 'attachment', module, id,
filename)
+ def admin(self, type = None, user = None):
+ if type:
+ if user:
+ return href_join(self.base, 'admin', type, user)
+ return href_join(self.base, 'admin', type+'/')
+ return href_join(self.base, 'admin/')
+
\ No newline at end of file
Index: templates/macros.cs
===================================================================
--- templates/macros.cs (revision 443)
+++ templates/macros.cs (working copy)
@@ -21,4 +21,83 @@
if:!part.last ?><span class="browser-pathsep">/</span><?cs /if ?><?cs
/each ?><?cs if:file.filename ?><span class="filename"><?cs
var:file.filename
?></span><?cs /if ?></div>
-<?cs /def ?>
\ No newline at end of file
+<?cs /def ?>
+
+<?cs def:hdf_editlist(enum) ?>
+ <input type="text" name="name"><br />
+ <table>
+ <tr><td>
+ <select size="5" multiple="true" name="selection">
+ <?cs each:item = $enum ?>
+ <option><?cs var:item.name ?></option>
+ <?cs /each ?>
+ </select>
+ </td><td>
+ <input type="submit" name="add" value="Add" /><br />
+ <input type="submit" name="update" value="Rename" /><br />
+ <input type="submit" name="delete" value="Remove" /><br />
+ </td></tr>
+ </table>
+<?cs /def?>
+
+<!--
+<?cs def:hdf_selector(available, selected) ?>
+ <script>
+ // Move a selected field from one select list to another.
+ function moveField(fromSelect, toSelect)
+ {
+ var fromSelectedIndex = fromSelect.selectedIndex;
+ // Obtain a reference to the option being moved.
+ if( fromSelectedIndex != -1 )
+ {
+ var o = fromSelect.options[fromSelectedIndex];
+ var o2 = new Option(o.text,o.value,false,false);
+
+ // Remove option from available list.
+ fromSelect.options[fromSelectedIndex] = null;
+
+ // Add the option to the end of the to list.
+ toSelect.options[toSelect.options.length] = o2;
+ }
+ }
+
+ // Move an item from available field list to selected field list.
+ function toSelected() {
+ var availableSelect =
window.document.displaySettingForm.availableSelect;
+ var displayedSelect =
window.document.displaySettingForm.displayedSelect;
+ moveField(availableSelect, displayedSelect);
+ }
+
+ // Move an item from displayed field list to the available field list.
+ function toAvailable() {
+ var displayedSelect =
window.document.displaySettingForm.displayedSelect;
+ var availableSelect =
window.document.displaySettingForm.availableSelect;
+ moveField(displayedSelect, availableSelect);
+ }
+
+
+ </script>
+ <table>
+ <tr>
+ <td>
+ Available Permissions:
+ <select>
+ <?cs each:item = $available ?>
+ <option id="<?cs var:item.name ?>"><?cs var:item.name ?></option>
+ <?cs /each ?>
+ </select>
+ </td><td valign="center">
+ <input type="button" value="==>" onclick="add_selected()" /><br />
+ <input type="button" value="<==" onclick="remove_selected()"
/><br />
+ </td><td>
+ Granted Permissions:
+ <select>
+ <?cs each:item = $selected ?>
+ <option id="<?cs var:item.name ?>"><?cs var:item.name ?></option>
+ <?cs /each ?>
+ </select>
+ </td>
+ </tr>
+ </table>
+<?cs /def ?>
+-->
Index: templates/header.cs
===================================================================
--- templates/header.cs (revision 443)
+++ templates/header.cs (working copy)
@@ -33,6 +33,8 @@
@import url("<?cs var:$htdocs_location ?>/css/report.css");
<?cs elif:trac.active_module == 'search' ?>
@import url("<?cs var:$htdocs_location ?>/css/search.css");
+ <?cs elif:trac.active_module == 'admin' ?>
+ @import url("<?cs var:$htdocs_location ?>/css/admin.css");
<?cs /if ?>
/* Dynamically/template-generated CSS below */
#navbar { background: url("<?cs var:$htdocs_location
?>/topbar_gradient.png") top left #f7f7f7 }
@@ -109,5 +111,7 @@
"TICKET_CREATE", "9") ?>
<?cs call:navlink("Search", $trac.href.search, "search",
"SEARCH_VIEW", "4") ?>
+ <?cs call:navlink("Admin", $trac.href.admin, "admin",
+ "TRAC_ADMIN", "") ?>
</ul>
</div>
\ No newline at end of file
I figured #11 (Web configuration/administration instead of trac-admin)
would be a good place to start ;-)
The attached patch is a first stab at exposing relevant functionality
from trac-admin via a new Admin module. The UI's not the greatest (and
not 100% consistent), there's very little error handling and it has a
couple of bugs:
- it only requires TRAC_ADMIN permission, and that's not fully enforced
due to a bug in the permissions code
- version and milestone times are being shifted on each update; probably
the rendering and parsing of times are applying the local timezone
differently, I haven't dug into it
- you can't delete multiple items; it's a UI design limitation for most
stuff, but for priorities, severities and permissions it's because I
haven't figured out a way to access all the values of a multi-valued
HTTP request parameter yet
- alot of code is copy/pasted from trac-admin; I need to go back and
refactor to extract a common, shared module if I carry on with this
So, not something 'ready to commit' but it's a start. I didn't want to
take it any further without some feedback. I achieved my basic goal of
getting some initial familiarity with the code base, so my question is:
should I take this further?
More specifically, Danial/Jonas/Rocky, are you interested in having
someone work on this? Would you place this above or below CVS
integration, which I'm also looking at (as Daniel already knows)?
If you'd be interested in seeing this go forward, I wouldn't mind
advice/suggestions on how the UI could improve, as well as code review
in case I'm doing anything horrible! :-)
Anyway, here it is:
L.
Index: trac/core.py
===================================================================
--- trac/core.py (revision 443)
+++ trac/core.py (working copy)
@@ -51,6 +51,7 @@
'changeset' : ('Changeset', 'Changeset', 1),
'newticket' : ('Ticket', 'Newticket', 0),
'attachment' : ('File', 'Attachment', 0),
+ 'admin' : ('Admin', 'Admin', 0),
}
def parse_path_info(path_info):
@@ -117,6 +118,12 @@
args['id'] = match.group(2)
args['filename'] = match.group(3)
return args
+ match = re.search('^/admin/([a-zA-Z_]+)?/?([a-zA-Z_]+)?', path_info)
+ if match:
+ args['mode'] = 'admin'
+ args['type'] = match.group(1)
+ args['user'] = match.group(2)
+ return args
return args
def parse_args(command, path_info, query_string,
@@ -199,6 +206,7 @@
hdf.setValue('project.name', env.get_config('project', 'name'))
hdf.setValue('project.descr', env.get_config('project', 'descr'))
+ hdf.setValue('trac.href.admin', env.href.admin())
hdf.setValue('trac.href.wiki', env.href.wiki())
hdf.setValue('trac.href.browser', env.href.browser('/'))
hdf.setValue('trac.href.timeline', env.href.timeline())
Index: trac/Admin.py
===================================================================
--- trac/Admin.py (revision 0)
+++ trac/Admin.py (revision 0)
@@ -0,0 +1,346 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2004 Edgewall Software
+# Copyright (C) 2004 Jonas Borgstrˆm <jonas@edgewall.com>
+# Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
+#
+# Trac is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 2 of the
+# License, or (at your option) any later version.
+#
+# Trac is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+# Author: Jonas Borgstrˆm <jonas@edgewall.com>
+
+import perm
+from Module import Module
+import neo_cgi
+import neo_cs
+import sqlite
+import time
+
+class Admin (Module):
+ template_name = 'admin.cs'
+
+ def render (self):
+ #self.perm.assert_permission(perm.TRAC_ADMIN) FIXME: fails to
find the permission?!
+ self.req.hdf.setValue('title', 'Admin')
+ type = self.args.get('type', 'components')
+ user = self.args.get('user')
+ if (type == None):
+ type='components'
+ action = self.get_action()
+
+ if action and action != 'view':
+ # Process requested updates
+ # TODO: validate inputs
+ handler = getattr(self, 'do_' + type)
+ handler(action)
+ self.req.redirect(self.env.href.admin(type, user))
+ else:
+ # Render the page
+ self.req.hdf.setValue('admin.type', type)
+ renderer = getattr(self, 'render_' + type)
+ renderer()
+
+ def get_action (self):
+ self.action = 'view'
+ if self.args.get('add'):
+ self.action = 'add'
+ elif self.args.get('update'):
+ self.action = 'update'
+ elif self.args.get('delete'):
+ self.action = 'delete'
+ return self.action
+
+
+ def do_components(self, action):
+ if action == 'add':
+ self.do_component_add()
+ elif action == 'update':
+ self.do_component_update()
+ elif action == 'delete':
+ self.do_component_delete()
+
+ def do_component_add(self):
+ name = self.args.get('name')
+ owner = self.args.get('owner')
+ self._db_execsql("INSERT INTO component VALUES('%s', '%s')"
+ % (name, owner))
+
+ def do_component_update(self):
+ name = self.args.get('name')
+ owner = self.args.get('owner')
+ newname = self.args.get('newname')
+ newowner = self.args.get('newowner')
+
+ if (name != newname) or (owner != newowner):
+ cnx = self.env.get_db_cnx()
+ cursor = cnx.cursor ()
+ if (owner != newowner):
+ self._db_execsql("UPDATE component SET owner='%s' WHERE
name='%s'"
+ % (newowner,name), cursor)
+ if (name != newname):
+ self._db_execsql("UPDATE component SET name='%s' WHERE
name='%s'"
+ % (newname,name), cursor)
+ self._db_execsql("UPDATE ticket SET component='%s'
WHERE component='%s'"
+ % (newname,name), cursor)
+ cnx.commit()
+
+ def do_component_delete(self):
+ name = self.args.get('name')
+ self._db_execsql("DELETE FROM component WHERE name='%s'"
+ % (name))
+
+
+ def do_versions(self, action):
+ if action == 'add':
+ self.do_mile_ver_add('version')
+ if action == 'update':
+ self.do_mile_ver_update('version')
+ if action == 'delete':
+ self.do_mile_ver_delete('version')
+
+ def do_milestones(self, action):
+ if action == 'add':
+ self.do_mile_ver_add('milestone')
+ if action == 'update':
+ self.do_mile_ver_update('milestone')
+ if action == 'delete':
+ self.do_mile_ver_delete('milestone')
+
+ def do_mile_ver_add(self, type):
+ name = self.args.get('name')
+ t = self.args.get('time')
+ seconds = self._parse_time(t)
+
+ self._db_execsql("INSERT INTO %(type)s('name','time') "
+ " VALUES('%(name)s', '%(time)s')"
+ % {'type':type, 'name':name, 'time':seconds})
+
+ def do_mile_ver_update(self, type):
+ name = self.args.get('name')
+ newname = self.args.get('newname')
+ t = self.args.get('newtime')
+ seconds = self._parse_time(t)
+
+ cnx = self.env.get_db_cnx()
+ cursor = cnx.cursor ()
+ self._db_execsql("UPDATE %(type)s SET name='%(newname)s'"
+ " WHERE name='%(name)s'"
+ % {'type':type, 'newname':newname,
'name':name},
+ cursor)
+ if seconds:
+ self._db_execsql("UPDATE %s SET time='%s'"
+ " WHERE name='%s'" % (type, seconds, newname),
+ cursor)
+ cnx.commit()
+
+ def do_mile_ver_delete(self, type):
+ name = self.args.get('name')
+ d = {'name':name, 'type':type}
+ data = self._db_execsql("SELECT name FROM %(type)s"
+ " WHERE name='%(name)s'" % d)
+ #if not data:
+ # raise Exception, "No such %s '%s'" % (type, name) FIXME:
clean error reporting
+ data = self._db_execsql("DELETE FROM %(type)s"
+ " WHERE name='%(name)s'" % d)
+
+
+ def do_priorities(self, action):
+ if action == 'add':
+ self.do_enum_add('priority')
+ if action == 'update':
+ self.do_enum_update('priority')
+ if action == 'delete':
+ self.do_enum_delete('priority')
+
+ def do_severities(self, action):
+ if action == 'add':
+ self.do_enum_add('severity')
+ if action == 'update':
+ self.do_enum_update('severity')
+ if action == 'delete':
+ self.do_enum_delete('severity')
+
+ def do_enum_add(self, type):
+ name = self.args.get('name')
+ sql = ("INSERT INTO enum('value','type','name') "
+ " SELECT 1+ifnull(max(value),0),'%(type)s','%(name)s'"
+ " FROM enum WHERE type='%(type)s'"
+ % {'type':type, 'name':name})
+ self._db_execsql(sql)
+ # FIXME enum already in response; have to do a refresh
+
+ def do_enum_update(self, type):
+ name = self.args.get('selection')
+ newname = self.args.get('name')
+ d = {'name':name, 'newname':newname, 'type':type}
+ data = self._db_execsql("SELECT name FROM enum"
+ " WHERE type='%(type)s' AND
name='%(name)s'" % d)
+ if not data:
+ raise Exception, "No such value '%s'" % name
+ data = self._db_execsql("UPDATE enum SET name='%(newname)s'"
+ " WHERE type='%(type)s' AND
name='%(name)s'" % d)
+
+ def do_enum_delete(self, type):
+ name = self.args.get('selection')
+ data = self._db_execsql("SELECT name FROM enum"
+ " WHERE type='%s' AND name='%s'" %
(type, name))
+ if not data:
+ raise Exception, "No such value '%s'" % name
+ data = self._db_execsql("DELETE FROM enum WHERE type='%s' AND
name='%s'"
+ % (type, name))
+
+
+ def do_permissions(self, action):
+ if action == 'add':
+ self.do_permission_add()
+ if action == 'delete':
+ self.do_permission_remove()
+
+ def do_permission_add(self):
+ user = self.args.get('user')
+ perms = self.args.get('permissions.avail')
+
+ #for action in perms:
+ action = perms
+ self._db_execsql("INSERT INTO permission VALUES('%s', '%s')" %
(user, action))
+
+
+ def do_permission_remove(self):
+ user = self.args.get('user')
+ perms = self.args.get('permissions.grant')
+
+ #for action in perms:
+ action = perms
+ self._db_execsql("DELETE FROM permission WHERE username='%s'
AND action='%s'" %
+ (user, action))
+
+
+ #
+ # Page rendering
+ #
+
+ def render_components (self):
+ idx = 0
+ components = self._db_execsql('SELECT name,owner FROM component
ORDER BY name')
+ for item in components:
+ self.req.hdf.setValue('admin.components.%d.name' % idx,
item[0])
+ self.req.hdf.setValue('admin.components.%d.owner' % idx,
item[1])
+ idx = idx + 1
+
+ def render_versions (self):
+ idx = 0
+ versions = self._db_execsql('SELECT name,time FROM version
ORDER BY time,name')
+ for item in versions:
+ secs = item[1]
+ st = time.gmtime(secs)
+ tm = time.strftime('%x %X', st)
+
+ self.req.hdf.setValue('admin.versions.%d.name' % idx, item[0])
+ self.req.hdf.setValue('admin.versions.%d.time' % idx, tm)
+
+ idx = idx + 1
+
+ def render_milestones (self):
+ idx = 0
+ milestones = self._db_execsql('SELECT name,time FROM milestone
ORDER BY time,name')
+ for item in milestones:
+ secs = item[1]
+ st = time.gmtime(secs)
+ tm = time.strftime('%x %X', st)
+
+ self.req.hdf.setValue('admin.milestones.%d.name' % idx,
item[0])
+ self.req.hdf.setValue('admin.milestones.%d.time' % idx, tm)
+ idx = idx + 1
+ self.req.hdf.setValue('admin.milestone.count', str(idx))
+
+ def render_priorities (self):
+ # data's already present, nothing to do
+ pass
+
+ def render_severities (self):
+ # data's already present, nothing to do
+ pass
+
+ def render_permissions(self):
+ user = self.args.get('user')
+ if user:
+ avail = perm.meta_permission.keys()
+ avail = avail + perm.base_permissions
+
+ data = self._db_execsql("SELECT action FROM permission "
+ "WHERE username='%s'"
+ "ORDER BY action" % user)
+
+ idx = 0
+ for row in data:
+ item = row[0]
+# if item in avail:
+# del avail[item]
+ self.req.hdf.setValue('admin.permissions.grant.%d.name'
% idx, item)
+ idx = idx + 1
+
+ idx = 0
+ avail.sort()
+ for item in avail:
+ self.req.hdf.setValue('admin.permissions.avail.%d.name'
%idx, item)
+ idx = idx + 1
+
+ self.req.hdf.setValue('admin.user', user)
+
+
+ #
+ # Utility methods
+ #
+
+ def _db_open(self):
+ try:
+ #if not self.__env:
+ # self.__env = trac.Environment.Environment (self.envname)
+ #return self.__env.get_db_cnx()
+ return self.env.get_db_cnx()
+ except Exception, e:
+ print 'Failed to open environment.', e
+ sys.exit(1)
+
+ def _db_execsql (self, sql, cursor=None):
+ data = []
+ if not cursor:
+ cnx = self._db_open()
+ cursor = cnx.cursor()
+ else:
+ cnx = None
+ cursor.execute(sql)
+ while 1:
+ row = cursor.fetchone()
+ if row == None:
+ break
+ data.append(row)
+ if cnx:
+ cnx.commit()
+ return data
+
+ def _parse_time(self, t):
+ seconds = None
+ if t == 'now':
+ seconds = str(int(time.time()))
+ else:
+ for format in ['%x %X', '%x, %X', '%X %x', '%X, %x', '%x']:
+ try:
+ seconds = str(time.mktime(time.strptime(t,
format))) #FIXME: time isn't round-tripping correctly
+ except ValueError:
+ continue
+
+ return seconds
+
+
\ No newline at end of file
Index: trac/perm.py
===================================================================
--- trac/perm.py (revision 443)
+++ trac/perm.py (working copy)
@@ -60,7 +60,17 @@
WIKI_ADMIN : [WIKI_VIEW, WIKI_CREATE, WIKI_MODIFY, WIKI_DELETE]
}
-
+base_permissions = [.
+ TIMELINE_VIEW, SEARCH_VIEW, CONFIG_VIEW,
+ LOG_VIEW, FILE_VIEW, CHANGESET_VIEW, BROWSER_VIEW,
+
+ TICKET_VIEW, TICKET_CREATE, TICKET_MODIFY,
+
+ REPORT_VIEW, REPORT_SQL_VIEW, REPORT_CREATE, REPORT_MODIFY,
REPORT_DELETE,
+
+ WIKI_VIEW, WIKI_CREATE, WIKI_MODIFY, WIKI_DELETE
+ ]
+
class PermissionError (StandardError):
"""Insufficient permissions to complete the operation"""
def __init__ (self, action):
Index: trac/Href.py
===================================================================
--- trac/Href.py (revision 443)
+++ trac/Href.py (working copy)
@@ -105,3 +105,10 @@
else:
return href_join(self.base, 'attachment', module, id,
filename)
+ def admin(self, type = None, user = None):
+ if type:
+ if user:
+ return href_join(self.base, 'admin', type, user)
+ return href_join(self.base, 'admin', type+'/')
+ return href_join(self.base, 'admin/')
+
\ No newline at end of file
Index: templates/macros.cs
===================================================================
--- templates/macros.cs (revision 443)
+++ templates/macros.cs (working copy)
@@ -21,4 +21,83 @@
if:!part.last ?><span class="browser-pathsep">/</span><?cs /if ?><?cs
/each ?><?cs if:file.filename ?><span class="filename"><?cs
var:file.filename
?></span><?cs /if ?></div>
-<?cs /def ?>
\ No newline at end of file
+<?cs /def ?>
+
+<?cs def:hdf_editlist(enum) ?>
+ <input type="text" name="name"><br />
+ <table>
+ <tr><td>
+ <select size="5" multiple="true" name="selection">
+ <?cs each:item = $enum ?>
+ <option><?cs var:item.name ?></option>
+ <?cs /each ?>
+ </select>
+ </td><td>
+ <input type="submit" name="add" value="Add" /><br />
+ <input type="submit" name="update" value="Rename" /><br />
+ <input type="submit" name="delete" value="Remove" /><br />
+ </td></tr>
+ </table>
+<?cs /def?>
+
+<!--
+<?cs def:hdf_selector(available, selected) ?>
+ <script>
+ // Move a selected field from one select list to another.
+ function moveField(fromSelect, toSelect)
+ {
+ var fromSelectedIndex = fromSelect.selectedIndex;
+ // Obtain a reference to the option being moved.
+ if( fromSelectedIndex != -1 )
+ {
+ var o = fromSelect.options[fromSelectedIndex];
+ var o2 = new Option(o.text,o.value,false,false);
+
+ // Remove option from available list.
+ fromSelect.options[fromSelectedIndex] = null;
+
+ // Add the option to the end of the to list.
+ toSelect.options[toSelect.options.length] = o2;
+ }
+ }
+
+ // Move an item from available field list to selected field list.
+ function toSelected() {
+ var availableSelect =
window.document.displaySettingForm.availableSelect;
+ var displayedSelect =
window.document.displaySettingForm.displayedSelect;
+ moveField(availableSelect, displayedSelect);
+ }
+
+ // Move an item from displayed field list to the available field list.
+ function toAvailable() {
+ var displayedSelect =
window.document.displaySettingForm.displayedSelect;
+ var availableSelect =
window.document.displaySettingForm.availableSelect;
+ moveField(displayedSelect, availableSelect);
+ }
+
+
+ </script>
+ <table>
+ <tr>
+ <td>
+ Available Permissions:
+ <select>
+ <?cs each:item = $available ?>
+ <option id="<?cs var:item.name ?>"><?cs var:item.name ?></option>
+ <?cs /each ?>
+ </select>
+ </td><td valign="center">
+ <input type="button" value="==>" onclick="add_selected()" /><br />
+ <input type="button" value="<==" onclick="remove_selected()"
/><br />
+ </td><td>
+ Granted Permissions:
+ <select>
+ <?cs each:item = $selected ?>
+ <option id="<?cs var:item.name ?>"><?cs var:item.name ?></option>
+ <?cs /each ?>
+ </select>
+ </td>
+ </tr>
+ </table>
+<?cs /def ?>
+-->
Index: templates/header.cs
===================================================================
--- templates/header.cs (revision 443)
+++ templates/header.cs (working copy)
@@ -33,6 +33,8 @@
@import url("<?cs var:$htdocs_location ?>/css/report.css");
<?cs elif:trac.active_module == 'search' ?>
@import url("<?cs var:$htdocs_location ?>/css/search.css");
+ <?cs elif:trac.active_module == 'admin' ?>
+ @import url("<?cs var:$htdocs_location ?>/css/admin.css");
<?cs /if ?>
/* Dynamically/template-generated CSS below */
#navbar { background: url("<?cs var:$htdocs_location
?>/topbar_gradient.png") top left #f7f7f7 }
@@ -109,5 +111,7 @@
"TICKET_CREATE", "9") ?>
<?cs call:navlink("Search", $trac.href.search, "search",
"SEARCH_VIEW", "4") ?>
+ <?cs call:navlink("Admin", $trac.href.admin, "admin",
+ "TRAC_ADMIN", "") ?>
</ul>
</div>
\ No newline at end of file