changeset 340:920efb8af63d demo

Merge with 8026872a10ee6f218890d64219d8f26e27dce415
author Marcin Kuzminski <marcin@python-works.com>
date Wed, 30 Jun 2010 17:16:12 +0200
parents 27ef9dd22ca2 (current diff) 8026872a10ee (diff)
children 3f50e44b41b4
files pylons_app/controllers/admin.py pylons_app/controllers/admin/repos.py pylons_app/controllers/admin/users.py pylons_app/controllers/permissions.py pylons_app/controllers/repos.py pylons_app/controllers/users.py
diffstat 20 files changed, 916 insertions(+), 534 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Tue Jun 29 21:14:21 2010 +0200
+++ b/.hgtags	Wed Jun 30 17:16:12 2010 +0200
@@ -1,13 +1,14 @@
-436bee78d81a02835837d11ef1bf6c308b866bb4 v0.1 Stable
-3142616771cd4721e5d5c403947674a7cc90ec99 v0.2 Stable
-e2600310e0b22f6c0946c5a4ad663f53fedb0dca v0.3
-aa5ef0f1554831500fb0acae0eed19955773105b v0.2
-aa5ef0f1554831500fb0acae0eed19955773105b v0.2
-0000000000000000000000000000000000000000 v0.2
-3142616771cd4721e5d5c403947674a7cc90ec99 v0.2 Stable
-0000000000000000000000000000000000000000 v0.2 Stable
-0000000000000000000000000000000000000000 v0.2
-3142616771cd4721e5d5c403947674a7cc90ec99 v0.2
-436bee78d81a02835837d11ef1bf6c308b866bb4 v0.1 Stable
-0000000000000000000000000000000000000000 v0.1 Stable
-436bee78d81a02835837d11ef1bf6c308b866bb4 v0.1
+436bee78d81a02835837d11ef1bf6c308b866bb4 v0.1.0
+3142616771cd4721e5d5c403947674a7cc90ec99 v0.2.0
+3380ca40cdbad1b5d993431d00636b7aa8ba496e v0.6.0
+50a39f923f311da3ac493cde8094d2b41afe4104 v0.6.8
+c097458480a5972dd75d5695b61e855fd0ab371e v0.7.0
+8bdec09436cb7e4a764bd2ba50b84060e30eb34f v0.7.1
+1a18994cdc3bdd156ee93c7c0fb8d94a88f1f640 v0.7.2
+a3a7c3e03b76ee264a828cb1087970bb98bbffcd v0.7.3
+58b46f9194c347641bfc9a26697ef413a4761971 v0.7.4
+710e7a75bb6b8346cee3bd0ddda67592e4790268 v0.7.5
+ca80f8c0056211dad33483a50b913593516d7a6c v0.7.6
+0cf49c29c846fefeb4e1a222e4b1850e9e3eaa62 v0.7.7
+702c7e565c56a49c89414e81f28571c8e5b67408 v0.7.8
+c12f4d19c95065f313eefcd45eac9ef507f5fa55 v0.7.9
--- a/pylons_app/config/routing.py	Tue Jun 29 21:14:21 2010 +0200
+++ b/pylons_app/config/routing.py	Wed Jun 30 17:16:12 2010 +0200
@@ -32,7 +32,7 @@
         return not cr(repo_name, config['base_path'])
  
     #REST routes
-    with map.submapper(path_prefix='/_admin', controller='repos') as m:
+    with map.submapper(path_prefix='/_admin', controller='pylons_app.controllers.admin.repos:ReposController') as m:
         m.connect("repos", "/repos",
              action="create", conditions=dict(method=["POST"]))
         m.connect("repos", "/repos",
@@ -67,11 +67,11 @@
              action="delete_perm_user", conditions=dict(method=["DELETE"],
                                                         function=check_repo))
         
-    map.resource('user', 'users', path_prefix='/_admin')
-    map.resource('permission', 'permissions', path_prefix='/_admin')
+    map.resource('user', 'users', controller='pylons_app.controllers.admin.users:UsersController', path_prefix='/_admin')
+    map.resource('permission', 'permissions', controller='pylons_app.controllers.admin.permissions:PermissionsController', path_prefix='/_admin')
     
     #ADMIN
-    with map.submapper(path_prefix='/_admin', controller='admin') as m:
+    with map.submapper(path_prefix='/_admin', controller='pylons_app.controllers.admin.admin:AdminController') as m:
         m.connect('admin_home', '', action='index')#main page
         m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
                   action='add_repo')
@@ -84,9 +84,11 @@
                 controller='feed', action='atom',
                 conditions=dict(function=check_repo))
     
+    #LOGIN/LOGOUT
     map.connect('login_home', '/login', controller='login')
     map.connect('logout_home', '/logout', controller='login', action='logout')
     
+    #OTHERS
     map.connect('changeset_home', '/{repo_name:.*}/changeset/{revision}',
                 controller='changeset', revision='tip',
                 conditions=dict(function=check_repo))
@@ -115,4 +117,12 @@
     map.connect('files_archive_home', '/{repo_name:.*}/archive/{revision}/{fileformat}',
                 controller='files', action='archivefile', revision='tip',
                 conditions=dict(function=check_repo))
+    map.connect('repo_settings_update', '/{repo_name:.*}/settings',
+                controller='settings', action="update",
+                conditions=dict(method=["PUT"], function=check_repo))
+    map.connect('repo_settings_home', '/{repo_name:.*}/settings',
+                controller='settings', action='index',
+                conditions=dict(function=check_repo))
+
+    
     return map
--- a/pylons_app/controllers/admin.py	Tue Jun 29 21:14:21 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-# admin controller for pylons 
-# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
- 
-# This program 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; version 2
-# of the License or (at your opinion) any later version of the license.
-# 
-# This program 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., 51 Franklin Street, Fifth Floor, Boston,
-# MA  02110-1301, USA.
-"""
-Created on April 7, 2010
-admin controller for pylons
-@author: marcink
-"""
-import logging
-from pylons import request, response, session, tmpl_context as c
-from pylons_app.lib.base import BaseController, render
-from pylons_app.model import meta
-from pylons_app.model.db import UserLog
-from webhelpers.paginate import Page
-from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator
-
-log = logging.getLogger(__name__)
-
-class AdminController(BaseController):
-    
-    @LoginRequired()
-    def __before__(self):
-        super(AdminController, self).__before__()
-    
-    @HasPermissionAllDecorator('hg.admin')        
-    def index(self):
-        sa = meta.Session
-                         
-        users_log = sa.query(UserLog).order_by(UserLog.action_date.desc())
-        p = int(request.params.get('page', 1))
-        c.users_log = Page(users_log, page=p, items_per_page=10)
-        c.log_data = render('admin/admin_log.html')
-        if request.params.get('partial'):
-            return c.log_data
-        return render('admin/admin.html')    
-                
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/controllers/admin/admin.py	Wed Jun 30 17:16:12 2010 +0200
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+# encoding: utf-8
+# admin controller for pylons 
+# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
+ 
+# This program 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; version 2
+# of the License or (at your opinion) any later version of the license.
+# 
+# This program 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., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+"""
+Created on April 7, 2010
+admin controller for pylons
+@author: marcink
+"""
+import logging
+from pylons import request, response, session, tmpl_context as c
+from pylons_app.lib.base import BaseController, render
+from pylons_app.model import meta
+from pylons_app.model.db import UserLog
+from webhelpers.paginate import Page
+from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator
+
+log = logging.getLogger(__name__)
+
+class AdminController(BaseController):
+    
+    @LoginRequired()
+    def __before__(self):
+        super(AdminController, self).__before__()
+    
+    @HasPermissionAllDecorator('hg.admin')        
+    def index(self):
+        sa = meta.Session
+                         
+        users_log = sa.query(UserLog).order_by(UserLog.action_date.desc())
+        p = int(request.params.get('page', 1))
+        c.users_log = Page(users_log, page=p, items_per_page=10)
+        c.log_data = render('admin/admin_log.html')
+        if request.params.get('partial'):
+            return c.log_data
+        return render('admin/admin.html')    
+                
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/controllers/admin/permissions.py	Wed Jun 30 17:16:12 2010 +0200
@@ -0,0 +1,90 @@
+#!/usr/bin/env python
+# encoding: utf-8
+# permissions controller for pylons
+# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
+ 
+# This program 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; version 2
+# of the License or (at your opinion) any later version of the license.
+# 
+# This program 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., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+"""
+Created on April 27, 2010
+permissions controller for pylons
+@author: marcink
+"""
+from formencode import htmlfill
+from pylons import request, session, tmpl_context as c, url
+from pylons.controllers.util import abort, redirect
+from pylons.i18n.translation import _
+from pylons_app.lib import helpers as h
+from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator
+from pylons_app.lib.base import BaseController, render
+from pylons_app.model.db import User, UserLog
+from pylons_app.model.forms import UserForm
+from pylons_app.model.user_model import UserModel
+import formencode
+import logging
+
+log = logging.getLogger(__name__)
+
+class PermissionsController(BaseController):
+    """REST Controller styled on the Atom Publishing Protocol"""
+    # To properly map this controller, ensure your config/routing.py
+    # file has a resource setup:
+    #     map.resource('permission', 'permissions')
+    
+    @LoginRequired()
+    #@HasPermissionAllDecorator('hg.admin')
+    def __before__(self):
+        c.admin_user = session.get('admin_user')
+        c.admin_username = session.get('admin_username')
+        super(PermissionsController, self).__before__()
+        
+    def index(self, format='html'):
+        """GET /permissions: All items in the collection"""
+        # url('permissions')
+        return render('admin/permissions/permissions.html')
+
+    def create(self):
+        """POST /permissions: Create a new item"""
+        # url('permissions')
+
+    def new(self, format='html'):
+        """GET /permissions/new: Form to create a new item"""
+        # url('new_permission')
+
+    def update(self, id):
+        """PUT /permissions/id: Update an existing item"""
+        # Forms posted to this method should contain a hidden field:
+        #    <input type="hidden" name="_method" value="PUT" />
+        # Or using helpers:
+        #    h.form(url('permission', id=ID),
+        #           method='put')
+        # url('permission', id=ID)
+
+    def delete(self, id):
+        """DELETE /permissions/id: Delete an existing item"""
+        # Forms posted to this method should contain a hidden field:
+        #    <input type="hidden" name="_method" value="DELETE" />
+        # Or using helpers:
+        #    h.form(url('permission', id=ID),
+        #           method='delete')
+        # url('permission', id=ID)
+
+    def show(self, id, format='html'):
+        """GET /permissions/id: Show a specific item"""
+        # url('permission', id=ID)
+
+    def edit(self, id, format='html'):
+        """GET /permissions/id/edit: Form to edit an existing item"""
+        # url('edit_permission', id=ID)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/controllers/admin/repos.py	Wed Jun 30 17:16:12 2010 +0200
@@ -0,0 +1,201 @@
+#!/usr/bin/env python
+# encoding: utf-8
+# repos controller for pylons
+# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
+# This program 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; version 2
+# of the License or (at your opinion) any later version of the license.
+# 
+# This program 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., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+"""
+Created on April 7, 2010
+admin controller for pylons
+@author: marcink
+"""
+from formencode import htmlfill
+from operator import itemgetter
+from pylons import request, response, session, tmpl_context as c, url
+from pylons.controllers.util import abort, redirect
+from pylons.i18n.translation import _
+from pylons_app.lib import helpers as h
+from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator
+from pylons_app.lib.base import BaseController, render
+from pylons_app.lib.utils import invalidate_cache
+from pylons_app.model.forms import RepoForm
+from pylons_app.model.hg_model import HgModel
+from pylons_app.model.repo_model import RepoModel
+import formencode
+import logging
+log = logging.getLogger(__name__)
+
+class ReposController(BaseController):
+    """REST Controller styled on the Atom Publishing Protocol"""
+    # To properly map this controller, ensure your config/routing.py
+    # file has a resource setup:
+    #     map.resource('repo', 'repos')
+    
+    @LoginRequired()
+    @HasPermissionAllDecorator('hg.admin')
+    def __before__(self):
+        c.admin_user = session.get('admin_user')
+        c.admin_username = session.get('admin_username')
+        super(ReposController, self).__before__()
+                
+    def index(self, format='html'):
+        """GET /repos: All items in the collection"""
+        # url('repos')
+        cached_repo_list = HgModel().get_repos()
+        c.repos_list = sorted(cached_repo_list, key=itemgetter('name_sort'))
+        return render('admin/repos/repos.html')
+    
+    def create(self):
+        """POST /repos: Create a new item"""
+        # url('repos')
+        repo_model = RepoModel()
+        _form = RepoForm()()
+        try:
+            form_result = _form.to_python(dict(request.POST))
+            repo_model.create(form_result, c.hg_app_user)
+            invalidate_cache('cached_repo_list')
+            h.flash(_('created repository %s') % form_result['repo_name'],
+                    category='success')
+                                                             
+        except formencode.Invalid as errors:
+            c.form_errors = errors.error_dict
+            c.new_repo = errors.value['repo_name']
+            return htmlfill.render(
+                 render('admin/repos/repo_add.html'),
+                defaults=errors.value,
+                encoding="UTF-8")        
+
+        except Exception:
+            h.flash(_('error occured during creation of repository %s') \
+                    % form_result['repo_name'], category='error')
+            
+        return redirect('repos')
+
+    def new(self, format='html'):
+        """GET /repos/new: Form to create a new item"""
+        new_repo = request.GET.get('repo', '')
+        c.new_repo = h.repo_name_slug(new_repo)
+
+        return render('admin/repos/repo_add.html')
+
+    def update(self, repo_name):
+        """PUT /repos/repo_name: Update an existing item"""
+        # Forms posted to this method should contain a hidden field:
+        #    <input type="hidden" name="_method" value="PUT" />
+        # Or using helpers:
+        #    h.form(url('repo', repo_name=ID),
+        #           method='put')
+        # url('repo', repo_name=ID)
+        repo_model = RepoModel()
+        _form = RepoForm(edit=True)()
+        try:
+            form_result = _form.to_python(dict(request.POST))
+            repo_model.update(repo_name, form_result)
+            invalidate_cache('cached_repo_list')
+            h.flash(_('Repository %s updated succesfully' % repo_name),
+                    category='success')
+                           
+        except formencode.Invalid as errors:
+            c.repo_info = repo_model.get(repo_name)
+            c.users_array = repo_model.get_users_js()
+            errors.value.update({'user':c.repo_info.user.username})
+            c.form_errors = errors.error_dict
+            return htmlfill.render(
+                 render('admin/repos/repo_edit.html'),
+                defaults=errors.value,
+                encoding="UTF-8")
+        except Exception:
+            h.flash(_('error occured during update of repository %s') \
+                    % form_result['repo_name'], category='error')
+        return redirect(url('repos'))
+    
+    def delete(self, repo_name):
+        """DELETE /repos/repo_name: Delete an existing item"""
+        # Forms posted to this method should contain a hidden field:
+        #    <input type="hidden" name="_method" value="DELETE" />
+        # Or using helpers:
+        #    h.form(url('repo', repo_name=ID),
+        #           method='delete')
+        # url('repo', repo_name=ID)
+    
+        h.flash(_('deleted repository %s  - disabled for demo :)') 
+                % repo_name, category='success')
+        return redirect(url('repos'))
+            
+        repo_model = RepoModel()
+        repo = repo_model.get(repo_name)
+        if not repo:
+            h.flash(_('%s repository is not mapped to db perhaps' 
+                      ' it was moved or renamed  from the filesystem'
+                      ' please run the application again'
+                      ' in order to rescan repositories') % repo_name,
+                      category='error')
+        
+            return redirect(url('repos'))
+        try:
+            repo_model.delete(repo)            
+            invalidate_cache('cached_repo_list')
+            h.flash(_('deleted repository %s') % repo_name, category='success')
+        except Exception:
+            h.flash(_('An error occured during deletion of %s') % repo_name,
+                    category='error')
+        
+        return redirect(url('repos'))
+        
+    def delete_perm_user(self, repo_name):
+        """
+        DELETE an existing repository permission user
+        @param repo_name:
+        """
+        
+        try:
+            repo_model = RepoModel()
+            repo_model.delete_perm_user(request.POST, repo_name)            
+        except Exception as e:
+            h.flash(_('An error occured during deletion of repository user'),
+                    category='error')
+        
+        
+    def show(self, repo_name, format='html'):
+        """GET /repos/repo_name: Show a specific item"""
+        # url('repo', repo_name=ID)
+        
+    def edit(self, repo_name, format='html'):
+        """GET /repos/repo_name/edit: Form to edit an existing item"""
+        # url('edit_repo', repo_name=ID)
+        repo_model = RepoModel()
+        c.repo_info = repo = repo_model.get(repo_name)
+        if not repo:
+            h.flash(_('%s repository is not mapped to db perhaps' 
+                      ' it was created or renamed from the filesystem'
+                      ' please run the application again'
+                      ' in order to rescan repositories') % repo_name,
+                      category='error')
+        
+            return redirect(url('repos'))        
+        defaults = c.repo_info.__dict__
+        defaults.update({'user':c.repo_info.user.username})
+        c.users_array = repo_model.get_users_js()
+        
+        for p in c.repo_info.repo2perm:
+            defaults.update({'perm_%s' % p.user.username: 
+                             p.permission.permission_name})
+            
+        return htmlfill.render(
+            render('admin/repos/repo_edit.html'),
+            defaults=defaults,
+            encoding="UTF-8",
+            force_defaults=False
+        )          
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/controllers/admin/users.py	Wed Jun 30 17:16:12 2010 +0200
@@ -0,0 +1,157 @@
+#!/usr/bin/env python
+# encoding: utf-8
+# users controller for pylons
+# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
+ 
+# This program 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; version 2
+# of the License or (at your opinion) any later version of the license.
+# 
+# This program 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., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+"""
+Created on April 4, 2010
+users controller for pylons
+@author: marcink
+"""
+from formencode import htmlfill
+from pylons import request, session, tmpl_context as c, url
+from pylons.controllers.util import abort, redirect
+from pylons.i18n.translation import _
+from pylons_app.lib import helpers as h
+from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator
+from pylons_app.lib.base import BaseController, render
+from pylons_app.model.db import User, UserLog
+from pylons_app.model.forms import UserForm
+from pylons_app.model.user_model import UserModel, DefaultUserException
+import formencode
+import logging
+
+log = logging.getLogger(__name__)
+
+class UsersController(BaseController):
+    """REST Controller styled on the Atom Publishing Protocol"""
+    # To properly map this controller, ensure your config/routing.py
+    # file has a resource setup:
+    #     map.resource('user', 'users')
+    
+    @LoginRequired()
+    @HasPermissionAllDecorator('hg.admin')
+    def __before__(self):
+        c.admin_user = session.get('admin_user')
+        c.admin_username = session.get('admin_username')
+        super(UsersController, self).__before__()
+    
+
+    def index(self, format='html'):
+        """GET /users: All items in the collection"""
+        # url('users')
+        
+        c.users_list = self.sa.query(User).all()     
+        return render('admin/users/users.html')
+    
+    def create(self):
+        """POST /users: Create a new item"""
+        # url('users')
+        
+        user_model = UserModel()
+        login_form = UserForm()()
+        try:
+            form_result = login_form.to_python(dict(request.POST))
+            user_model.create(form_result)
+            h.flash(_('created user %s') % form_result['username'],
+                    category='success')
+        except formencode.Invalid as errors:
+            c.form_errors = errors.error_dict
+            return htmlfill.render(
+                 render('admin/users/user_add.html'),
+                defaults=errors.value,
+                encoding="UTF-8")
+        except Exception:
+            h.flash(_('error occured during creation of user %s') \
+                    % form_result['username'], category='error')            
+        return redirect(url('users'))
+    
+    def new(self, format='html'):
+        """GET /users/new: Form to create a new item"""
+        # url('new_user')
+        return render('admin/users/user_add.html')
+
+    def update(self, id):
+        """PUT /users/id: Update an existing item"""
+        # Forms posted to this method should contain a hidden field:
+        #    <input type="hidden" name="_method" value="PUT" />
+        # Or using helpers:
+        #    h.form(url('user', id=ID),
+        #           method='put')
+        # url('user', id=ID)
+        user_model = UserModel()
+        _form = UserForm(edit=True)()
+        try:
+            form_result = _form.to_python(dict(request.POST))
+            user_model.update(id, form_result)
+            h.flash(_('User updated succesfully'), category='success')
+                           
+        except formencode.Invalid as errors:
+            c.user = user_model.get_user(id)
+            c.form_errors = errors.error_dict
+            return htmlfill.render(
+                 render('admin/users/user_edit.html'),
+                defaults=errors.value,
+                encoding="UTF-8")
+        except Exception:
+            h.flash(_('error occured during update of user %s') \
+                    % form_result['username'], category='error')
+            
+        return redirect(url('users'))
+    
+    def delete(self, id):
+        """DELETE /users/id: Delete an existing item"""
+        # Forms posted to this method should contain a hidden field:
+        #    <input type="hidden" name="_method" value="DELETE" />
+        # Or using helpers:
+        #    h.form(url('user', id=ID),
+        #           method='delete')
+        # url('user', id=ID)
+        h.flash(_('sucessfully deleted user - disabled for demo :)'), category='success')
+        return redirect(url('users'))
+        user_model = UserModel()
+        try:
+            user_model.delete(id)
+            h.flash(_('sucessfully deleted user'), category='success')
+        except DefaultUserException as e:
+            h.flash(str(e), category='warning')
+        except Exception:
+            h.flash(_('An error occured during deletion of user'),
+                    category='error')            
+        return redirect(url('users'))
+        
+    def show(self, id, format='html'):
+        """GET /users/id: Show a specific item"""
+        # url('user', id=ID)
+    
+    
+    def edit(self, id, format='html'):
+        """GET /users/id/edit: Form to edit an existing item"""
+        # url('edit_user', id=ID)
+        c.user = self.sa.query(User).get(id)
+        if c.user.username == 'default':
+            h.flash(_("You can't edit this user since it's" 
+              " crucial for entire application"), category='warning')
+            return redirect(url('users'))
+        
+        defaults = c.user.__dict__
+        return htmlfill.render(
+            render('admin/users/user_edit.html'),
+            defaults=defaults,
+            encoding="UTF-8",
+            force_defaults=False
+        )    
--- a/pylons_app/controllers/changelog.py	Tue Jun 29 21:14:21 2010 +0200
+++ b/pylons_app/controllers/changelog.py	Wed Jun 30 17:16:12 2010 +0200
@@ -72,7 +72,7 @@
         if not repo.revisions:return dumps([]), 0
         
         max_rev = repo.revisions[-1]
-        offset = 1 if p == 1 else  ((p - 1) * revcount)
+        offset = 1 if p == 1 else  ((p - 1) * revcount + 1)
         rev_start = repo.revisions[(-1 * offset)]
         
         revcount = min(max_rev, revcount)
--- a/pylons_app/controllers/permissions.py	Tue Jun 29 21:14:21 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-# permissions controller for pylons
-# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
- 
-# This program 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; version 2
-# of the License or (at your opinion) any later version of the license.
-# 
-# This program 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., 51 Franklin Street, Fifth Floor, Boston,
-# MA  02110-1301, USA.
-"""
-Created on April 27, 2010
-permissions controller for pylons
-@author: marcink
-"""
-from formencode import htmlfill
-from pylons import request, session, tmpl_context as c, url
-from pylons.controllers.util import abort, redirect
-from pylons.i18n.translation import _
-from pylons_app.lib import helpers as h
-from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator
-from pylons_app.lib.base import BaseController, render
-from pylons_app.model.db import User, UserLog
-from pylons_app.model.forms import UserForm
-from pylons_app.model.user_model import UserModel
-import formencode
-import logging
-
-log = logging.getLogger(__name__)
-
-class PermissionsController(BaseController):
-    """REST Controller styled on the Atom Publishing Protocol"""
-    # To properly map this controller, ensure your config/routing.py
-    # file has a resource setup:
-    #     map.resource('permission', 'permissions')
-    
-    @LoginRequired()
-    #@HasPermissionAllDecorator('hg.admin')
-    def __before__(self):
-        c.admin_user = session.get('admin_user')
-        c.admin_username = session.get('admin_username')
-        super(PermissionsController, self).__before__()
-        
-    def index(self, format='html'):
-        """GET /permissions: All items in the collection"""
-        # url('permissions')
-        return render('admin/permissions/permissions.html')
-
-    def create(self):
-        """POST /permissions: Create a new item"""
-        # url('permissions')
-
-    def new(self, format='html'):
-        """GET /permissions/new: Form to create a new item"""
-        # url('new_permission')
-
-    def update(self, id):
-        """PUT /permissions/id: Update an existing item"""
-        # Forms posted to this method should contain a hidden field:
-        #    <input type="hidden" name="_method" value="PUT" />
-        # Or using helpers:
-        #    h.form(url('permission', id=ID),
-        #           method='put')
-        # url('permission', id=ID)
-
-    def delete(self, id):
-        """DELETE /permissions/id: Delete an existing item"""
-        # Forms posted to this method should contain a hidden field:
-        #    <input type="hidden" name="_method" value="DELETE" />
-        # Or using helpers:
-        #    h.form(url('permission', id=ID),
-        #           method='delete')
-        # url('permission', id=ID)
-
-    def show(self, id, format='html'):
-        """GET /permissions/id: Show a specific item"""
-        # url('permission', id=ID)
-
-    def edit(self, id, format='html'):
-        """GET /permissions/id/edit: Form to edit an existing item"""
-        # url('edit_permission', id=ID)
--- a/pylons_app/controllers/repos.py	Tue Jun 29 21:14:21 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,201 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-# repos controller for pylons
-# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
-# This program 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; version 2
-# of the License or (at your opinion) any later version of the license.
-# 
-# This program 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., 51 Franklin Street, Fifth Floor, Boston,
-# MA  02110-1301, USA.
-"""
-Created on April 7, 2010
-admin controller for pylons
-@author: marcink
-"""
-from formencode import htmlfill
-from operator import itemgetter
-from pylons import request, response, session, tmpl_context as c, url
-from pylons.controllers.util import abort, redirect
-from pylons.i18n.translation import _
-from pylons_app.lib import helpers as h
-from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator
-from pylons_app.lib.base import BaseController, render
-from pylons_app.lib.utils import invalidate_cache
-from pylons_app.model.forms import RepoForm
-from pylons_app.model.hg_model import HgModel
-from pylons_app.model.repo_model import RepoModel
-import formencode
-import logging
-log = logging.getLogger(__name__)
-
-class ReposController(BaseController):
-    """REST Controller styled on the Atom Publishing Protocol"""
-    # To properly map this controller, ensure your config/routing.py
-    # file has a resource setup:
-    #     map.resource('repo', 'repos')
-    
-    @LoginRequired()
-    @HasPermissionAllDecorator('hg.admin')
-    def __before__(self):
-        c.admin_user = session.get('admin_user')
-        c.admin_username = session.get('admin_username')
-        super(ReposController, self).__before__()
-                
-    def index(self, format='html'):
-        """GET /repos: All items in the collection"""
-        # url('repos')
-        cached_repo_list = HgModel().get_repos()
-        c.repos_list = sorted(cached_repo_list, key=itemgetter('name_sort'))
-        return render('admin/repos/repos.html')
-    
-    def create(self):
-        """POST /repos: Create a new item"""
-        # url('repos')
-        repo_model = RepoModel()
-        _form = RepoForm()()
-        try:
-            form_result = _form.to_python(dict(request.POST))
-            repo_model.create(form_result, c.hg_app_user)
-            invalidate_cache('cached_repo_list')
-            h.flash(_('created repository %s') % form_result['repo_name'],
-                    category='success')
-                                                             
-        except formencode.Invalid as errors:
-            c.form_errors = errors.error_dict
-            c.new_repo = errors.value['repo_name']
-            return htmlfill.render(
-                 render('admin/repos/repo_add.html'),
-                defaults=errors.value,
-                encoding="UTF-8")        
-
-        except Exception:
-            h.flash(_('error occured during creation of repository %s') \
-                    % form_result['repo_name'], category='error')
-            
-        return redirect('repos')
-
-    def new(self, format='html'):
-        """GET /repos/new: Form to create a new item"""
-        new_repo = request.GET.get('repo', '')
-        c.new_repo = h.repo_name_slug(new_repo)
-
-        return render('admin/repos/repo_add.html')
-
-    def update(self, repo_name):
-        """PUT /repos/repo_name: Update an existing item"""
-        # Forms posted to this method should contain a hidden field:
-        #    <input type="hidden" name="_method" value="PUT" />
-        # Or using helpers:
-        #    h.form(url('repo', repo_name=ID),
-        #           method='put')
-        # url('repo', repo_name=ID)
-        repo_model = RepoModel()
-        _form = RepoForm(edit=True)()
-        try:
-            form_result = _form.to_python(dict(request.POST))
-            repo_model.update(repo_name, form_result)
-            invalidate_cache('cached_repo_list')
-            h.flash(_('Repository %s updated succesfully' % repo_name),
-                    category='success')
-                           
-        except formencode.Invalid as errors:
-            c.repo_info = repo_model.get(repo_name)
-            c.users_array = repo_model.get_users_js()
-            errors.value.update({'user':c.repo_info.user.username})
-            c.form_errors = errors.error_dict
-            return htmlfill.render(
-                 render('admin/repos/repo_edit.html'),
-                defaults=errors.value,
-                encoding="UTF-8")
-        except Exception:
-            h.flash(_('error occured during update of repository %s') \
-                    % form_result['repo_name'], category='error')
-        return redirect(url('repos'))
-    
-    def delete(self, repo_name):
-        """DELETE /repos/repo_name: Delete an existing item"""
-        # Forms posted to this method should contain a hidden field:
-        #    <input type="hidden" name="_method" value="DELETE" />
-        # Or using helpers:
-        #    h.form(url('repo', repo_name=ID),
-        #           method='delete')
-        # url('repo', repo_name=ID)
-    
-        h.flash(_('deleted repository %s  - disabled for demo :)') 
-                % repo_name, category='success')
-        return redirect(url('repos'))
-            
-        repo_model = RepoModel()
-        repo = repo_model.get(repo_name)
-        if not repo:
-            h.flash(_('%s repository is not mapped to db perhaps' 
-                      ' it was moved or renamed  from the filesystem'
-                      ' please run the application again'
-                      ' in order to rescan repositories') % repo_name,
-                      category='error')
-        
-            return redirect(url('repos'))
-        try:
-            repo_model.delete(repo)            
-            invalidate_cache('cached_repo_list')
-            h.flash(_('deleted repository %s') % repo_name, category='success')
-        except Exception:
-            h.flash(_('An error occured during deletion of %s') % repo_name,
-                    category='error')
-        
-        return redirect(url('repos'))
-        
-    def delete_perm_user(self, repo_name):
-        """
-        DELETE an existing repository permission user
-        @param repo_name:
-        """
-        
-        try:
-            repo_model = RepoModel()
-            repo_model.delete_perm_user(request.POST, repo_name)            
-        except Exception as e:
-            h.flash(_('An error occured during deletion of repository user'),
-                    category='error')
-        
-        
-    def show(self, repo_name, format='html'):
-        """GET /repos/repo_name: Show a specific item"""
-        # url('repo', repo_name=ID)
-        
-    def edit(self, repo_name, format='html'):
-        """GET /repos/repo_name/edit: Form to edit an existing item"""
-        # url('edit_repo', repo_name=ID)
-        repo_model = RepoModel()
-        c.repo_info = repo = repo_model.get(repo_name)
-        if not repo:
-            h.flash(_('%s repository is not mapped to db perhaps' 
-                      ' it was created or renamed from the filesystem'
-                      ' please run the application again'
-                      ' in order to rescan repositories') % repo_name,
-                      category='error')
-        
-            return redirect(url('repos'))        
-        defaults = c.repo_info.__dict__
-        defaults.update({'user':c.repo_info.user.username})
-        c.users_array = repo_model.get_users_js()
-        
-        for p in c.repo_info.repo2perm:
-            defaults.update({'perm_%s' % p.user.username: 
-                             p.permission.permission_name})
-            
-        return htmlfill.render(
-            render('admin/repos/repo_edit.html'),
-            defaults=defaults,
-            encoding="UTF-8",
-            force_defaults=False
-        )          
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/controllers/settings.py	Wed Jun 30 17:16:12 2010 +0200
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+# encoding: utf-8
+# settings controller for pylons
+# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
+ 
+# This program 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; version 2
+# of the License or (at your opinion) any later version of the license.
+# 
+# This program 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., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+"""
+Created on June 30, 2010
+settings controller for pylons
+@author: marcink
+"""
+from formencode import htmlfill
+from pylons import tmpl_context as c, request, url
+from pylons.controllers.util import redirect
+from pylons.i18n.translation import _
+from pylons_app.lib.auth import LoginRequired, HasRepoPermissionAllDecorator
+from pylons_app.lib.base import BaseController, render
+from pylons_app.lib.utils import invalidate_cache
+from pylons_app.model.forms import RepoSettingsForm
+from pylons_app.model.repo_model import RepoModel
+import formencode
+import logging
+import pylons_app.lib.helpers as h
+log = logging.getLogger(__name__)
+
+class SettingsController(BaseController):
+
+    @LoginRequired()
+    @HasRepoPermissionAllDecorator('repository.admin')           
+    def __before__(self):
+        super(SettingsController, self).__before__()
+        
+    def index(self, repo_name):
+        repo_model = RepoModel()
+        c.repo_info = repo = repo_model.get(repo_name)
+        if not repo:
+            h.flash(_('%s repository is not mapped to db perhaps' 
+                      ' it was created or renamed from the filesystem'
+                      ' please run the application again'
+                      ' in order to rescan repositories') % repo_name,
+                      category='error')
+        
+            return redirect(url('repos'))        
+        defaults = c.repo_info.__dict__
+        defaults.update({'user':c.repo_info.user.username})
+        c.users_array = repo_model.get_users_js()
+        
+        for p in c.repo_info.repo2perm:
+            defaults.update({'perm_%s' % p.user.username: 
+                             p.permission.permission_name})
+            
+        return htmlfill.render(
+            render('settings/repo_settings.html'),
+            defaults=defaults,
+            encoding="UTF-8",
+            force_defaults=False
+        )  
+
+    def update(self, repo_name):
+        print request.POST
+        print 'x' * 110
+        repo_model = RepoModel()
+        _form = RepoSettingsForm(edit=True)()
+        try:
+            form_result = _form.to_python(dict(request.POST))
+            repo_model.update(repo_name, form_result)
+            invalidate_cache('cached_repo_list')
+            h.flash(_('Repository %s updated succesfully' % repo_name),
+                    category='success')
+                           
+        except formencode.Invalid as errors:
+            c.repo_info = repo_model.get(repo_name)
+            c.users_array = repo_model.get_users_js()
+            errors.value.update({'user':c.repo_info.user.username})
+            c.form_errors = errors.error_dict
+            return htmlfill.render(
+                 render('admin/repos/repo_edit.html'),
+                defaults=errors.value,
+                encoding="UTF-8")
+        except Exception:
+            h.flash(_('error occured during update of repository %s') \
+                    % form_result['repo_name'], category='error')
+                    
+        return redirect(url('repo_settings_home', repo_name=repo_name))
--- a/pylons_app/controllers/users.py	Tue Jun 29 21:14:21 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,157 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-# users controller for pylons
-# Copyright (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
- 
-# This program 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; version 2
-# of the License or (at your opinion) any later version of the license.
-# 
-# This program 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., 51 Franklin Street, Fifth Floor, Boston,
-# MA  02110-1301, USA.
-"""
-Created on April 4, 2010
-users controller for pylons
-@author: marcink
-"""
-from formencode import htmlfill
-from pylons import request, session, tmpl_context as c, url
-from pylons.controllers.util import abort, redirect
-from pylons.i18n.translation import _
-from pylons_app.lib import helpers as h
-from pylons_app.lib.auth import LoginRequired, HasPermissionAllDecorator
-from pylons_app.lib.base import BaseController, render
-from pylons_app.model.db import User, UserLog
-from pylons_app.model.forms import UserForm
-from pylons_app.model.user_model import UserModel, DefaultUserException
-import formencode
-import logging
-
-log = logging.getLogger(__name__)
-
-class UsersController(BaseController):
-    """REST Controller styled on the Atom Publishing Protocol"""
-    # To properly map this controller, ensure your config/routing.py
-    # file has a resource setup:
-    #     map.resource('user', 'users')
-    
-    @LoginRequired()
-    @HasPermissionAllDecorator('hg.admin')
-    def __before__(self):
-        c.admin_user = session.get('admin_user')
-        c.admin_username = session.get('admin_username')
-        super(UsersController, self).__before__()
-    
-
-    def index(self, format='html'):
-        """GET /users: All items in the collection"""
-        # url('users')
-        
-        c.users_list = self.sa.query(User).all()     
-        return render('admin/users/users.html')
-    
-    def create(self):
-        """POST /users: Create a new item"""
-        # url('users')
-        
-        user_model = UserModel()
-        login_form = UserForm()()
-        try:
-            form_result = login_form.to_python(dict(request.POST))
-            user_model.create(form_result)
-            h.flash(_('created user %s') % form_result['username'],
-                    category='success')
-        except formencode.Invalid as errors:
-            c.form_errors = errors.error_dict
-            return htmlfill.render(
-                 render('admin/users/user_add.html'),
-                defaults=errors.value,
-                encoding="UTF-8")
-        except Exception:
-            h.flash(_('error occured during creation of user %s') \
-                    % form_result['username'], category='error')            
-        return redirect(url('users'))
-    
-    def new(self, format='html'):
-        """GET /users/new: Form to create a new item"""
-        # url('new_user')
-        return render('admin/users/user_add.html')
-
-    def update(self, id):
-        """PUT /users/id: Update an existing item"""
-        # Forms posted to this method should contain a hidden field:
-        #    <input type="hidden" name="_method" value="PUT" />
-        # Or using helpers:
-        #    h.form(url('user', id=ID),
-        #           method='put')
-        # url('user', id=ID)
-        user_model = UserModel()
-        _form = UserForm(edit=True)()
-        try:
-            form_result = _form.to_python(dict(request.POST))
-            user_model.update(id, form_result)
-            h.flash(_('User updated succesfully'), category='success')
-                           
-        except formencode.Invalid as errors:
-            c.user = user_model.get_user(id)
-            c.form_errors = errors.error_dict
-            return htmlfill.render(
-                 render('admin/users/user_edit.html'),
-                defaults=errors.value,
-                encoding="UTF-8")
-        except Exception:
-            h.flash(_('error occured during update of user %s') \
-                    % form_result['username'], category='error')
-            
-        return redirect(url('users'))
-    
-    def delete(self, id):
-        """DELETE /users/id: Delete an existing item"""
-        # Forms posted to this method should contain a hidden field:
-        #    <input type="hidden" name="_method" value="DELETE" />
-        # Or using helpers:
-        #    h.form(url('user', id=ID),
-        #           method='delete')
-        # url('user', id=ID)
-        h.flash(_('sucessfully deleted user - disabled for demo :)'), category='success')
-        return redirect(url('users'))
-        user_model = UserModel()
-        try:
-            user_model.delete(id)
-            h.flash(_('sucessfully deleted user'), category='success')
-        except DefaultUserException as e:
-            h.flash(str(e), category='warning')
-        except Exception:
-            h.flash(_('An error occured during deletion of user'),
-                    category='error')            
-        return redirect(url('users'))
-        
-    def show(self, id, format='html'):
-        """GET /users/id: Show a specific item"""
-        # url('user', id=ID)
-    
-    
-    def edit(self, id, format='html'):
-        """GET /users/id/edit: Form to edit an existing item"""
-        # url('edit_user', id=ID)
-        c.user = self.sa.query(User).get(id)
-        if c.user.username == 'default':
-            h.flash(_("You can't edit this user since it's" 
-              " crucial for entire application"), category='warning')
-            return redirect(url('users'))
-        
-        defaults = c.user.__dict__
-        return htmlfill.render(
-            render('admin/users/user_edit.html'),
-            defaults=defaults,
-            encoding="UTF-8",
-            force_defaults=False
-        )    
--- a/pylons_app/model/forms.py	Tue Jun 29 21:14:21 2010 +0200
+++ b/pylons_app/model/forms.py	Wed Jun 30 17:16:12 2010 +0200
@@ -182,7 +182,15 @@
                                      state=State_obj)
                 raise formencode.Invalid(msg, value, state, error_dict={'perm_new_user_name':msg})            
         return value
-                
+    
+class ValidSettings(formencode.validators.FancyValidator):
+    
+    def to_python(self, value, state):
+        #settings  form can't edit user
+        if value.has_key('user'):
+            del['value']['user']
+        
+        return value                
 #===============================================================================
 # FORMS        
 #===============================================================================
@@ -240,3 +248,18 @@
         
         chained_validators = [ValidPerms]
     return _RepoForm
+
+def RepoSettingsForm(edit=False):
+    class _RepoForm(formencode.Schema):
+        allow_extra_fields = True
+        filter_extra_fields = False
+        repo_name = All(UnicodeString(strip=True, min=1, not_empty=True), ValidRepoName(edit))
+        description = UnicodeString(strip=True, min=3, not_empty=True)
+        private = StringBoolean(if_missing=False)
+        
+        chained_validators = [ValidPerms, ValidSettings]
+    return _RepoForm
+
+
+
+
--- a/pylons_app/public/css/monoblue_custom.css	Tue Jun 29 21:14:21 2010 +0200
+++ b/pylons_app/public/css/monoblue_custom.css	Wed Jun 30 17:16:12 2010 +0200
@@ -742,9 +742,6 @@
 }
 
 /** end of changeset **/ /** canvas **/
-#graph_nodes {
-	margin-top: 8px;
-}
 
 #graph {
 	overflow: hidden;
@@ -753,6 +750,8 @@
 #graph_nodes {
 	width: 160px;
 	float: left;
+	margin-left:-50px;
+	margin-top: 5px;
 }
 
 #graph_content {
@@ -774,7 +773,8 @@
 	border-bottom: 1px solid #CCCCCC;
 	border-left: 1px solid #CCCCCC;
 	border-right: 1px solid #CCCCCC;
-	height: 120px;
+	min-height: 80px;
+	overflow: hidden;
 }
 
 #graph_content .container .left {
--- a/pylons_app/public/js/graph.js	Tue Jun 29 21:14:21 2010 +0200
+++ b/pylons_app/public/js/graph.js	Wed Jun 30 17:16:12 2010 +0200
@@ -1,5 +1,6 @@
 // branch_renderer.js - Rendering of branch DAGs on the client side
 //
+// Copyright 2010 Marcin Kuzminski <marcin AT python-works DOT com>
 // Copyright 2008 Jesper Noehr <jesper AT noehr DOT org>
 // Copyright 2008 Dirkjan Ochtman <dirkjan AT ochtman DOT nl>
 // Copyright 2006 Alexander Schremmer <alex AT alexanderweb DOT de>
@@ -32,7 +33,8 @@
 	this.ctx.fillStyle = 'rgb(0, 0, 0)';
 	this.cur = [0, 0];
 	this.max_column = 1;
-	this.line_width = 3;
+	this.line_width = 2.5;
+	this.dot_radius = 5.5;
 	this.bg = [0, 4];
 	this.cell = [2, 0];
 	this.revlink = '';
@@ -60,7 +62,7 @@
 		var idx = 1;
 		var rela = document.getElementById('graph');
 		var pad = 160;
-		var scale = 20;
+		var scale = 22;
 		
 		for (var i in data) {
 			this.scale(scale);
@@ -102,20 +104,21 @@
 				
 				y = row.offsetTop-rela.offsetTop+4;
 				x = pad-((this.cell[0] + this.box_size * start - 1) + this.bg_height-2);
+				this.ctx.lineWidth=this.line_width;
 				this.ctx.beginPath();
 				this.ctx.moveTo(x, y);
 				
 				//i don't know why it's +1 just fixes some drawing graph.
 				y += row.clientHeight+1;
 				x = pad-((1 + this.box_size * end) + this.bg_height-2);
-				this.ctx.lineTo(x,y+extra);
+				this.ctx.lineTo(x,y+extra,3);
 				this.ctx.stroke();
 			}
 			
 			column = node[0]
 			color = node[1]
 			
-			radius = 4;
+			radius = this.dot_radius;
 			y = row.offsetTop-rela.offsetTop+4;
 			x = pad-(Math.round(this.cell[0] * scale/2 * column + radius) + 15 - (column*4));
 		
--- a/pylons_app/templates/base/base.html	Tue Jun 29 21:14:21 2010 +0200
+++ b/pylons_app/templates/base/base.html	Wed Jun 30 17:16:12 2010 +0200
@@ -107,7 +107,7 @@
 	            <li ${is_current('tags')}>${h.link_to(_('tags'),h.url('tags_home',repo_name=c.repo_name))}</li>
 	            <li ${is_current('files')}>${h.link_to(_('files'),h.url('files_home',repo_name=c.repo_name))}</li>
 				%if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
-					<li>${h.link_to(_('settings'),h.url('edit_repo',repo_name=c.repo_name))}</li>
+					<li ${is_current('settings')}>${h.link_to(_('settings'),h.url('repo_settings_home',repo_name=c.repo_name))}</li>
 				%endif					        
 	        </ul>
 		%else:
--- a/pylons_app/templates/changelog/changelog.html	Tue Jun 29 21:14:21 2010 +0200
+++ b/pylons_app/templates/changelog/changelog.html	Wed Jun 30 17:16:12 2010 +0200
@@ -42,17 +42,17 @@
 		<div id="chg_${cnt+1}" class="container">
 			<div class="left">
 				<div class="date">${_('commit')} ${cs.revision}: ${cs.raw_id}@${cs.date}</div>
+					<span class="logtags">
+						<span class="branchtag">${cs.branch}</span>
+						%for tag in cs.tags:
+							<span class="tagtag">${tag}</span>
+						%endfor
+					</span>					
 				<div class="author">${cs.author}</div>
 				<div class="message">
 					${h.link_to(h.wrap_paragraphs(cs.message),
 					h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
 				</div>
-					<span class="logtags">
-						<span class="branchtag">${cs.branch}</span>
-						%for tag in cs.tags:
-							<span class="tagtag">${tag}</span>
-						%endfor
-					</span>
 			</div>	
 			<div class="right">
 						<div class="changes">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/templates/settings/repo_settings.html	Wed Jun 30 17:16:12 2010 +0200
@@ -0,0 +1,240 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/base/base.html"/>
+
+<%def name="title()">
+    ${_('Repository settings')}
+</%def>
+<%def name="breadcrumbs()">
+    ${h.link_to(u'Home',h.url('/'))}
+    / 
+    ${h.link_to(c.repo_name,h.url('shortlog_home',repo_name=c.repo_name))}
+    /
+    ${_('settings')}
+</%def>
+<%def name="page_nav()">
+	${self.menu('settings')}     
+</%def>
+<%def name="main()">
+	<h2 class="no-link no-border">${_('Settings')}</h2>
+	<div>
+        ${h.form(url('repo_settings_update', repo_name=c.repo_info.repo_name),method='put')}
+        <table>
+        	<tr>
+        		<td>${_('Name')}</td>
+        		<td>${h.text('repo_name',size="28")}</td>
+        		<td>${self.get_form_error('repo_name')}</td>
+        	</tr>
+        	<tr>
+        		<td>${_('Description')}</td>
+        		<td>${h.textarea('description',cols=32,rows=5)}</td>
+        		<td>${self.get_form_error('description')}</td>
+        	</tr>
+        	<tr>
+        		<td>${_('Private')}</td>
+        		<td>${h.checkbox('private',value="True")}</td>
+        		<td>${self.get_form_error('private')}</td>
+        	</tr>
+        	<tr>
+        		<td>${_('Permissions')}</td>
+        		<td>
+        			<table>
+        				<tr>
+        					<td>${_('none')}</td>
+        					<td>${_('read')}</td>
+        					<td>${_('write')}</td>
+        					<td>${_('admin')}</td>
+        					<td>${_('user')}</td>
+        				</tr>
+        				
+        				%for r2p in c.repo_info.repo2perm:
+        					%if r2p.user.username =='default' and c.repo_info.private:
+        						<tr>
+									<td colspan="4">
+										<span style="font-size: 0.8em">${_('disabled for private repository')}</span></td>
+									<td>${r2p.user.username}</td>
+								</tr>
+							%else:
+	        				<tr id=${id(r2p.user.username)}>
+	        					<td>${h.radio('perm_%s' % r2p.user.username,'repository.none')}</td>
+	        					<td>${h.radio('perm_%s' % r2p.user.username,'repository.read')}</td>
+	        					<td>${h.radio('perm_%s' % r2p.user.username,'repository.write')}</td>
+	        					<td>${h.radio('perm_%s' % r2p.user.username,'repository.admin')}</td>
+	        					<td>${r2p.user.username}</td>
+	        					<td>
+	        					  %if r2p.user.username !='default':
+				                  	<span class="delete_icon action_button" onclick="ajaxAction(${r2p.user.user_id},${id(r2p.user.username)})">
+				                  		<script type="text/javascript">
+											function ajaxAction(user_id,field_id){
+												var sUrl = "${h.url('delete_repo_user',repo_name=c.repo_name)}";
+												var callback = { success:function(o){
+																YAHOO.util.Dom.get(String(field_id)).innerHTML = '<td colspan="6"></td>';
+															 }};
+												var postData = '_method=delete&user_id='+user_id; 
+												var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData); 
+						                	};
+										</script>       	
+				                  	</span>
+				                  %endif					
+	        					</td>
+	        				</tr>
+	        				%endif
+						%endfor
+						<%
+							if not hasattr(c,'form_errors'):
+								d = 'display:none;'
+							else:
+								d=''
+						%>
+
+        				<tr id="add_perm_input" style="${d}">
+        					<td>${h.radio('perm_new_user','repository.none')}</td>
+        					<td>${h.radio('perm_new_user','repository.read')}</td>
+        					<td>${h.radio('perm_new_user','repository.write')}</td>
+        					<td>${h.radio('perm_new_user','repository.admin')}</td>
+        					<td class='ac'>
+        						<div id="perm_ac">
+        							${h.text('perm_new_user_name',class_='yui-ac-input')}
+									<div id="perm_container"></div>
+        						</div>
+        					</td>
+        					<td>${self.get_form_error('perm_new_user_name')}</td>     					
+        				</tr>
+        				<tr>
+        					<td colspan="4">
+        						<span id="add_perm" class="add_icon" style="cursor: pointer;">
+        						${_('Add another user')}
+        						</span>
+        					</td>
+        				</tr>
+        			</table>
+        		</td>
+        		
+        	</tr>
+        	<tr>
+        		<td></td>
+        		<td>${h.submit('update','update')}</td>
+        	</tr>
+        	        	        	
+        </table>
+        ${h.end_form()}
+        <script type="text/javascript">
+        	YAHOO.util.Event.onDOMReady(function(){
+				var D = YAHOO.util.Dom;
+				YAHOO.util.Event.addListener('add_perm','click',function(){
+					D.setStyle('add_perm_input','display','');
+					D.setStyle('add_perm','opacity','0.6');
+					D.setStyle('add_perm','cursor','default');
+				});
+            });
+        </script>
+		<script type="text/javascript">    
+		YAHOO.example.FnMultipleFields = function(){
+		    var myContacts = ${c.users_array|n}
+		    
+		    // Define a custom search function for the DataSource
+		    var matchNames = function(sQuery) {
+		        // Case insensitive matching
+		        var query = sQuery.toLowerCase(),
+		            contact,
+		            i=0,
+		            l=myContacts.length,
+		            matches = [];
+		        
+		        // Match against each name of each contact
+		        for(; i<l; i++) {
+		            contact = myContacts[i];
+		            if((contact.fname.toLowerCase().indexOf(query) > -1) ||
+		                (contact.lname.toLowerCase().indexOf(query) > -1) ||
+		                (contact.nname && (contact.nname.toLowerCase().indexOf(query) > -1))) {
+		                matches[matches.length] = contact;
+		            }
+		        }
+		
+		        return matches;
+		    };
+		
+		    // Use a FunctionDataSource
+		    var oDS = new YAHOO.util.FunctionDataSource(matchNames);
+		    oDS.responseSchema = {
+		        fields: ["id", "fname", "lname", "nname"]
+		    }
+		
+		    // Instantiate AutoComplete for perms
+		    var oAC_perms = new YAHOO.widget.AutoComplete("perm_new_user_name", "perm_container", oDS);
+		    oAC_perms.useShadow = false;
+		    oAC_perms.resultTypeList = false;
+		    
+		    // Instantiate AutoComplete for owner
+		 	var oAC_owner = new YAHOO.widget.AutoComplete("user", "owner_container", oDS);
+		 	oAC_owner.useShadow = false;
+		 	oAC_owner.resultTypeList = false;
+		    
+		    
+		    // Custom formatter to highlight the matching letters
+		    var custom_formatter = function(oResultData, sQuery, sResultMatch) {
+		        var query = sQuery.toLowerCase(),
+		            fname = oResultData.fname,
+		            lname = oResultData.lname,
+		            nname = oResultData.nname || "", // Guard against null value
+		            query = sQuery.toLowerCase(),
+		            fnameMatchIndex = fname.toLowerCase().indexOf(query),
+		            lnameMatchIndex = lname.toLowerCase().indexOf(query),
+		            nnameMatchIndex = nname.toLowerCase().indexOf(query),
+		            displayfname, displaylname, displaynname;
+		            
+		        if(fnameMatchIndex > -1) {
+		            displayfname = highlightMatch(fname, query, fnameMatchIndex);
+		        }
+		        else {
+		            displayfname = fname;
+		        }
+		
+		        if(lnameMatchIndex > -1) {
+		            displaylname = highlightMatch(lname, query, lnameMatchIndex);
+		        }
+		        else {
+		            displaylname = lname;
+		        }
+		
+		        if(nnameMatchIndex > -1) {
+		            displaynname = "(" + highlightMatch(nname, query, nnameMatchIndex) + ")";
+		        }
+		        else {
+		            displaynname = nname ? "(" + nname + ")" : "";
+		        }
+		
+		        return displayfname + " " + displaylname + " " + displaynname;
+		        
+		    };
+		    oAC_perms.formatResult = custom_formatter; 
+		    oAC_owner.formatResult = custom_formatter;
+		    			    
+		    // Helper function for the formatter
+		    var highlightMatch = function(full, snippet, matchindex) {
+		        return full.substring(0, matchindex) + 
+		                "<span class='match'>" + 
+		                full.substr(matchindex, snippet.length) + 
+		                "</span>" +
+		                full.substring(matchindex + snippet.length);
+		    };
+		
+		    var myHandler = function(sType, aArgs) {
+		        var myAC = aArgs[0]; // reference back to the AC instance
+		        var elLI = aArgs[1]; // reference to the selected LI element
+		        var oData = aArgs[2]; // object literal of selected item's result data
+		        myAC.getInputEl().value = oData.nname;
+		    };
+
+		    oAC_perms.itemSelectEvent.subscribe(myHandler);
+		    oAC_owner.itemSelectEvent.subscribe(myHandler);
+		    
+		    return {
+		        oDS: oDS,
+		        oAC_perms: oAC_perms,
+		        oAC_owner: oAC_owner, 
+		    };
+		}();
+		    
+		</script>        
+    </div>
+</%def>   
--- a/pylons_app/templates/shortlog/shortlog.html	Tue Jun 29 21:14:21 2010 +0200
+++ b/pylons_app/templates/shortlog/shortlog.html	Wed Jun 30 17:16:12 2010 +0200
@@ -1,3 +1,4 @@
+## -*- coding: utf-8 -*-
 <%inherit file="/base/base.html"/>
 
 <%def name="title()">
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pylons_app/tests/functional/test_settings.py	Wed Jun 30 17:16:12 2010 +0200
@@ -0,0 +1,7 @@
+from pylons_app.tests import *
+
+class TestSettingsController(TestController):
+
+    def test_index(self):
+        response = self.app.get(url(controller='settings', action='index'))
+        # Test response...