[ckan-changes] commit/ckan: 2 new changesets

Bitbucket commits-noreply at bitbucket.org
Mon Oct 10 11:14:08 UTC 2011


2 new changesets in ckan:

http://bitbucket.org/okfn/ckan/changeset/b2458c0730dd/
changeset:   b2458c0730dd
branch:      feature-1264-core-admin
user:        zephod
date:        2011-10-10 13:13:43
summary:     [close-branch]:
affected #:  0 files (-1 bytes)

http://bitbucket.org/okfn/ckan/changeset/b216952644aa/
changeset:   b216952644aa
user:        zephod
date:        2011-10-10 13:14:06
summary:     [merge,from-branch]: Admin merged into core.
affected #:  15 files (-1 bytes)

--- a/ckan/config/routing.py	Mon Oct 10 11:36:03 2011 +0300
+++ b/ckan/config/routing.py	Mon Oct 10 12:14:06 2011 +0100
@@ -256,6 +256,9 @@
     map.connect('/revision/diff/{id}', controller='revision', action='diff')
     map.connect('/revision/list', controller='revision', action='list')
     map.connect('/revision/{id}', controller='revision', action='read')
+
+    map.connect('ckanadmin_index', '/ckan-admin', controller='admin', action='index')
+    map.connect('ckanadmin', '/ckan-admin/{action}', controller='admin')
     
     for plugin in routing_plugins:
         map = plugin.after_map(map)


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/controllers/admin.py	Mon Oct 10 12:14:06 2011 +0100
@@ -0,0 +1,297 @@
+from ckan.lib.base import *
+import ckan.authz
+import ckan.lib.authztool
+import ckan.model as model
+
+from ckan.model.authz import Role
+roles = Role.get_all()
+role_tuples = [(x,x) for x in roles]
+
+def get_sysadmins():
+    q = model.Session.query(model.SystemRole).filter_by(role=model.Role.ADMIN)
+    return [uor.user for uor in q.all() if uor.user]
+
+
+class AdminController(BaseController):
+    def __before__(self, action, **params):
+        super(AdminController, self).__before__(action, **params)
+        if not ckan.authz.Authorizer().is_sysadmin(unicode(c.user)):
+            abort(401, 'Need to be system administrator to administer')        
+        c.revision_change_state_allowed = (
+            c.user and
+            self.authorizer.is_authorized(c.user, model.Action.CHANGE_STATE,
+                model.Revision)
+            )
+
+    def index(self):
+        #now pass the list of sysadmins 
+        c.sysadmins = [a.name for a in get_sysadmins()]
+   
+        return render('admin/index.html')
+
+
+    def authz(self):
+        def action_save_form(users_or_authz_groups):
+            # The permissions grid has been saved
+            # which is a grid of checkboxes named user$role
+            rpi = request.params.items()
+
+            # The grid passes us a list of the users/roles that were displayed
+            submitted = [ a for (a,b) in rpi if (b == u'submitted')]
+            # and also those which were checked
+            checked = [ a for (a,b) in rpi if (b == u'on')]
+
+            # from which we can deduce true/false for each user/role combination
+            # that was displayed in the form
+            table_dict={}
+            for a in submitted:
+                table_dict[a]=False
+            for a in checked:
+                table_dict[a]=True
+
+            # now we'll split up the user$role strings to make a dictionary from 
+            # (user,role) to True/False, which tells us what we need to do.
+            new_user_role_dict={}
+            for (ur,val) in table_dict.items():
+                u,r = ur.split('$')
+                new_user_role_dict[(u,r)] = val
+               
+            # we get the current user/role assignments 
+            # and make a dictionary of them
+            current_uors = model.Session.query(model.SystemRole).all()
+
+            if users_or_authz_groups=='users':
+                current_users_roles = [( uor.user.name, uor.role) for uor in current_uors if uor.user]
+            elif users_or_authz_groups=='authz_groups':
+                current_users_roles = [( uor.authorized_group.name, uor.role) for uor in current_uors if uor.authorized_group]        
+            else:
+                assert False, "shouldn't be here"
+
+            current_user_role_dict={}
+            for (u,r) in current_users_roles:
+                current_user_role_dict[(u,r)]=True
+
+            # and now we can loop through our dictionary of desired states
+            # checking whether a change needs to be made, and if so making it
+
+            # WORRY: Here it seems that we have to check whether someone is already assigned
+            # a role, in order to avoid assigning it twice, or attempting to delete it when
+            # it doesn't exist. Otherwise problems occur. However this doesn't affect the 
+            # index page, which would seem to be prone to suffer the same effect. 
+            # Why the difference?
+
+            if users_or_authz_groups=='users':
+                for ((u,r), val) in new_user_role_dict.items():
+                    if val:
+                        if not ((u,r) in current_user_role_dict):
+                            model.add_user_to_role(model.User.by_name(u),r,model.System())
+                    else:
+                        if ((u,r) in current_user_role_dict):
+                            model.remove_user_from_role(model.User.by_name(u),r,model.System())
+            elif users_or_authz_groups=='authz_groups':
+                for ((u,r), val) in new_user_role_dict.items():
+                    if val:
+                        if not ((u,r) in current_user_role_dict):
+                            model.add_authorization_group_to_role(model.AuthorizationGroup.by_name(u),r,model.System())
+                    else:
+                        if ((u,r) in current_user_role_dict):
+                            model.remove_authorization_group_from_role(model.AuthorizationGroup.by_name(u),r,model.System())
+            else:
+                assert False, "shouldn't be here"
+
+
+            # finally commit the change to the database
+            model.Session.commit()
+            h.flash_success("Changes Saved")
+
+        if ('save' in request.POST):
+            action_save_form('users')
+
+        if ('authz_save' in request.POST):
+            action_save_form('authz_groups')
+
+
+
+
+        def action_add_form(users_or_authz_groups):
+            # The user is attempting to set new roles for a named user
+            new_user = request.params.get('new_user_name')
+            # this is the list of roles whose boxes were ticked
+            checked_roles = [ a for (a,b) in request.params.items() if (b == u'on')]
+            # this is the list of all the roles that were in the submitted form
+            submitted_roles = [ a for (a,b) in request.params.items() if (b == u'submitted')]
+
+            # from this we can make a dictionary of the desired states
+            # i.e. true for the ticked boxes, false for the unticked
+            desired_roles = {}
+            for r in submitted_roles:
+                desired_roles[r]=False
+            for r in checked_roles:
+                desired_roles[r]=True
+
+            # again, in order to avoid either creating a role twice or deleting one which is
+            # non-existent, we need to get the users' current roles (if any)
+            
+            current_uors = model.Session.query(model.SystemRole).all()
+
+            if users_or_authz_groups=='users':
+                current_roles = [uor.role for uor in current_uors if ( uor.user and uor.user.name == new_user )]
+                user_object = model.User.by_name(new_user)
+                if user_object==None:
+                    # The submitted user does not exist. Bail with flash message
+                    h.flash_error('unknown user:' + str (new_user))
+                else:
+                    # Whenever our desired state is different from our current state, change it.
+                    for (r,val) in desired_roles.items():
+                        if val:
+                            if (r not in current_roles):
+                                model.add_user_to_role(user_object, r, model.System())
+                        else:
+                            if (r in current_roles):
+                                model.remove_user_from_role(user_object, r, model.System())
+                    h.flash_success("User Added")
+
+            elif users_or_authz_groups=='authz_groups':
+                current_roles = [uor.role for uor in current_uors if ( uor.authorized_group and uor.authorized_group.name == new_user )]
+                user_object = model.AuthorizationGroup.by_name(new_user)
+                if user_object==None:
+                    # The submitted user does not exist. Bail with flash message
+                    h.flash_error('unknown authorization group:' + str (new_user))
+                else:
+                    # Whenever our desired state is different from our current state, change it.
+                    for (r,val) in desired_roles.items():
+                        if val:
+                            if (r not in current_roles):
+                                model.add_authorization_group_to_role(user_object, r, model.System())
+                        else:
+                            if (r in current_roles):
+                                model.remove_authorization_group_from_role(user_object, r, model.System())
+                    h.flash_success("Authorization Group Added")
+
+
+            else:
+                assert False, "shouldn't be here"
+
+
+
+
+
+
+
+
+
+
+            # and finally commit all these changes to the database
+            model.Session.commit()
+
+        if 'add' in request.POST:
+            action_add_form('users')
+        if 'authz_add' in request.POST:
+            action_add_form('authz_groups')
+
+
+        # =================
+        # Display the page
+
+        # Find out all the possible roles. For the system object that's just all of them.
+        possible_roles = Role.get_all()
+
+        # get the list of users who have roles on the System, with their roles
+        uors = model.Session.query(model.SystemRole).all()
+        # uniquify and sort
+        users = sorted(list(set([uor.user.name for uor in uors if uor.user])))
+        authz_groups = sorted(list(set([uor.authorized_group.name for uor in uors if uor.authorized_group])))
+
+        # make a dictionary from (user, role) to True, False
+        users_roles = [( uor.user.name, uor.role) for uor in uors if uor.user]
+        user_role_dict={}
+        for u in users:
+            for r in possible_roles:
+                if (u,r) in users_roles:
+                    user_role_dict[(u,r)]=True
+                else:
+                    user_role_dict[(u,r)]=False
+
+
+        # and similarly make a dictionary from (authz_group, role) to True, False
+        authz_groups_roles = [( uor.authorized_group.name, uor.role) for uor in uors if uor.authorized_group]
+        authz_groups_role_dict={}
+        for u in authz_groups:
+            for r in possible_roles:
+                if (u,r) in authz_groups_roles:
+                    authz_groups_role_dict[(u,r)]=True
+                else:
+                    authz_groups_role_dict[(u,r)]=False
+
+        
+
+        # pass these variables to the template for rendering
+        c.roles = possible_roles
+
+        c.users = users
+        c.user_role_dict = user_role_dict
+
+        c.authz_groups = authz_groups
+        c.authz_groups_role_dict = authz_groups_role_dict
+    
+        return render('admin/authz.html')
+
+    def trash(self):
+        c.deleted_revisions = model.Session.query(
+                model.Revision).filter_by(state=model.State.DELETED)
+        c.deleted_packages = model.Session.query(
+                model.Package).filter_by(state=model.State.DELETED)
+        if not request.params:
+            return render('admin/trash.html')
+        else:
+            # NB: we repeat retrieval of of revisions
+            # this is obviously inefficient (but probably not *that* bad)
+            # but has to be done to avoid (odd) sqlalchemy errors (when doing
+            # purge packages) of form: "this object already exists in the
+            # session"
+            msgs = []
+            if ('purge-packages' in request.params) or ('purge-revisions' in request.params):
+                if 'purge-packages' in request.params:
+                    revs_to_purge = []
+                    for pkg in c.deleted_packages:
+                        revisions = [ x[0] for x in pkg.all_related_revisions ]
+                        # ensure no accidental purging of other(non-deleted) packages
+                        # initially just avoided purging revisions where
+                        # non-deleted packages were affected
+                        # however this lead to confusing outcomes e.g.
+                        # we succesfully deleted revision in which package was deleted (so package
+                        # now active again) but no other revisions
+                        problem = False
+                        for r in revisions:
+                            affected_pkgs = set(r.packages).difference(set(c.deleted_packages))
+                            if affected_pkgs:
+                                msg = _('Cannot purge package %s as ' + \
+                                    'associated revision %s includes non-deleted packages %s')
+                                msg = msg % (pkg.id, r.id, [pkg.id for r in affected_pkgs])
+                                msgs.append(msg)
+                                problem = True
+                                break
+                        if not problem:
+                            revs_to_purge += [ r.id for r in revisions ]
+                    model.Session.remove()
+                else:
+                    revs_to_purge = [ rev.id for rev in c.deleted_revisions ]
+                revs_to_purge = list(set(revs_to_purge))
+                for id in revs_to_purge:
+                    revision = model.Session.query(model.Revision).get(id)
+                    try:
+                        # TODO deleting the head revision corrupts the edit page
+                        # Ensure that whatever 'head' pointer is used gets moved down to the next revision
+                        model.repo.purge_revision(revision, leave_record=False)
+                    except Exception, inst:
+                        msg = 'Problem purging revision %s: %s' % (id,
+                                inst)
+                        msgs.append(msg)
+                h.flash_success(_('Purge complete'))
+            else:
+                msgs.append('Action not implemented.')
+
+            for msg in msgs:
+                h.flash_error(msg)
+            h.redirect_to(h.url_for('ckanadmin', action='trash'))
+


--- a/ckan/lib/helpers.py	Mon Oct 10 11:36:03 2011 +0300
+++ b/ckan/lib/helpers.py	Mon Oct 10 12:14:06 2011 +0100
@@ -264,3 +264,7 @@
 def time_ago_in_words_from_str(date_str, granularity='month'):
     return date.time_ago_in_words(date_str_to_datetime(date_str), granularity=granularity)
 
+def button_attr(enable, type='primary'):
+    if enable:
+        return 'class="pretty-button %s"' % type
+    return 'disabled class="pretty-button disabled"'


--- a/ckan/model/__init__.py	Mon Oct 10 11:36:03 2011 +0300
+++ b/ckan/model/__init__.py	Mon Oct 10 12:14:06 2011 +0100
@@ -230,6 +230,8 @@
                                 continue
                             if 'pending' not in obj.state:
                                 obj.current = True
+                                import datetime
+                                obj.expired_timestamp = datetime.datetime(9999, 12, 31)
                                 self.session.add(obj)
                                 break
                 # now delete revision object


--- a/ckan/public/css/style.css	Mon Oct 10 11:36:03 2011 +0300
+++ b/ckan/public/css/style.css	Mon Oct 10 12:14:06 2011 +0100
@@ -1078,9 +1078,6 @@
 }
   
 
-/* ================================== */
-/* = Twitter.Bootstrap Form Buttons = */
-/* ================================== */
 div.form-submit {
   background: #eee;
   padding: 20px;
@@ -1098,6 +1095,30 @@
   clear: both;
 }
 
+/* ==================== */
+/* = Multi-form pages = */
+/* ==================== */
+body.admin form#form-purge-packages, 
+body.admin form#form-purge-revisions {
+  margin-bottom: 30px;
+  text-align: right;
+}
+body.admin .actions button, 
+body.admin .actions input {
+  margin: 0;
+}
+body.admin.authz form {
+  margin-bottom: 30px;
+}
+body.admin.authz form button {
+  width: 120px;
+  float: right;
+}
+
+
+/* ================================== */
+/* = Twitter.Bootstrap Form Buttons = */
+/* ================================== */
 .pretty-button {
   cursor: pointer;
   display: inline-block;


--- a/ckan/templates/_util.html	Mon Oct 10 11:36:03 2011 +0300
+++ b/ckan/templates/_util.html	Mon Oct 10 12:14:06 2011 +0100
@@ -52,46 +52,46 @@
   <ul py:def="package_list(packages)" class="datasets"><li py:for="package in packages"
         class="${'fullyopen' if (package.isopen() and package.resources) else None}">
-        <div class="header">
-			<span class="title">
-				${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}
-			</span>
-			
-			<div class="search_meta">
-        <py:if test="package.resources">
-          <ul class="dataset_formats">
-            <py:for each="resource in package.resources">
-              <py:if test="resource.format and not resource.format == ''">
-                <li><a href="${resource.url}"
-                  title="${resource.description}">${resource.format}</a></li>
-              </py:if>
-            </py:for>
+      <div class="header">
+        <span class="title">
+          ${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}
+        </span>
+        
+        <div class="search_meta">
+          <py:if test="package.resources">
+            <ul class="dataset_formats">
+              <py:for each="resource in package.resources">
+                <py:if test="resource.format and not resource.format == ''">
+                  <li><a href="${resource.url}"
+                    title="${resource.description}">${resource.format}</a></li>
+                </py:if>
+              </py:for>
+            </ul>
+          </py:if>
+          <ul class="openness">
+            <py:if test="package.isopen()">
+              <li>
+                <a href="http://opendefinition.org/okd/" title="This dataset satisfies the Open Definition.">
+                    <img src="http://assets.okfn.org/images/ok_buttons/od_80x15_blue.png" alt="[Open Data]" />
+                </a>
+              </li>
+            </py:if>
+            <py:if test="not package.isopen()">
+              <li>
+                <span class="closed">
+                  ${h.icon('lock')} Not Openly Licensed
+                </span>
+              </li>
+            </py:if></ul>
-        </py:if>
-        <ul class="openness">
-          <py:if test="package.isopen()">
-            <li>
-              <a href="http://opendefinition.org/okd/" title="This dataset satisfies the Open Definition.">
-                  <img src="http://assets.okfn.org/images/ok_buttons/od_80x15_blue.png" alt="[Open Data]" />
-              </a>
-            </li>
-          </py:if>
-          <py:if test="not package.isopen()">
-            <li>
-              <span class="closed">
-                ${h.icon('lock')} Not Openly Licensed
-              </span>
-            </li>
-          </py:if>
-        </ul>
+        </div></div>
-		</div>
-		<div class="extract">
-			${h.markdown_extract(package.notes)}
-		</div>
-        <!--ul py:if="package.tags" class="tags">
-          <li py:for="tag in package.tags">${tag.name}</li>
-        </ul-->
+      <div class="extract">
+        ${h.markdown_extract(package.notes)}
+      </div>
+          <!--ul py:if="package.tags" class="tags">
+            <li py:for="tag in package.tags">${tag.name}</li>
+          </ul--></li></ul>
 
@@ -99,11 +99,11 @@
     <li py:for="package in packages"
         class="${'fullyopen' if (package.isopen and package.get('resources')) else None}"><div class="header">
-			<span class="title">
-				${h.link_to(package.get('title') or package.get('name'), h.url_for(controller='package', action='read', id=package.get('name')))}
-			</span>
-			
-			<div class="search_meta">
+      <span class="title">
+        ${h.link_to(package.get('title') or package.get('name'), h.url_for(controller='package', action='read', id=package.get('name')))}
+      </span>
+      
+      <div class="search_meta"><py:if test="package.resources"><ul class="dataset_formats"><py:for each="resource in package.resources">
@@ -364,12 +364,15 @@
             action="${h.url_for(controller='revision',
                 action='edit',
                 id=revision.id)}"
+            id="undelete-${revision.id}"
             ><py:if test="revision.state!='deleted'">
-            <button type="submit" name="action" value="delete">Delete</button>
+            <input type="hidden" name="action" value="delete"/>
+            <input type="submit" name="submit" value="Delete" class="pretty-button small" /></py:if><py:if test="revision.state=='deleted'">
-            <button type="submit" name="action" value="undelete">Undelete</button>
+            <input type="hidden" name="action" value="undelete"/>
+            <input type="submit" name="submit" value="Undelete" class="pretty-button small" /></py:if></form></div>
@@ -387,6 +390,9 @@
       </td><td>${revision.message}</td></tr>
+    <tr py:if="not any(revisions)" class="table-empty">
+      <td colspan="5">(none)</td>
+    </tr></table>
 
   
@@ -416,10 +422,10 @@
                 id=revision['id'])}"
             ><py:if test="revision['state']!='deleted'">
-            <button type="submit" name="action" value="delete">Delete</button>
+            <button type="submit" name="action" value="delete" class="pretty-button small">Delete</button></py:if><py:if test="revision['state']=='deleted'">
-            <button type="submit" name="action" value="undelete">Undelete</button>
+            <button type="submit" name="action" value="undelete" class="pretty-button small">Undelete</button></py:if></form></div>


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/admin/authz.html	Mon Oct 10 12:14:06 2011 +0100
@@ -0,0 +1,52 @@
+<html xmlns:py="http://genshi.edgewall.org/"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:xi="http://www.w3.org/2001/XInclude"
+  py:strip="">
+
+  <py:def function="page_title">Administration - Authorization</py:def>
+  <py:def function="page_heading">Administration - Authorization</py:def>
+
+  <div py:match="content">
+    <h3>Update Existing Roles</h3>
+
+    <form id="theform" method="POST">
+      ${authz_form_table('theform', c.roles, c.users, c.user_role_dict)}
+      <button type="submit" name="save" class="pretty-button primary">
+        Save Changes
+      </button>
+      <div class="clear"></div>
+    </form>
+
+    <h3>Add Roles for Any User</h3>
+    <form id="addform" method="POST">
+      ${authz_add_table(c.roles)}
+      <button type="submit" name="add" class="pretty-button primary">Add Role</button>
+      <div class="clear"></div>
+    </form>
+
+    <hr/>
+
+    <h3>Existing Roles for Authorization Groups</h3>
+
+    <form id="authzgroup_form" method="POST">
+      ${authz_form_group_table('authzgroup_form', c.roles, c.authz_groups, c.authz_groups_role_dict)}
+      <button type="submit" name="authz_save" class="pretty-button primary">Save Changes</button>
+      <div class="clear"></div>
+    </form>
+
+    <h3>Add Roles for Any Authorization Group</h3>
+
+    <form id="authzgroup_addform" method="POST">
+      ${authz_add_group_table(c.roles)}
+      <button type="submit" name="authz_add" class="pretty-button primary">Add Role</button>
+      <div class="clear"></div>
+    </form>
+
+
+
+
+  </div>
+
+  <xi:include href="layout.html" />
+</html>
+


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/admin/index.html	Mon Oct 10 12:14:06 2011 +0100
@@ -0,0 +1,23 @@
+<html xmlns:py="http://genshi.edgewall.org/"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:xi="http://www.w3.org/2001/XInclude"
+  py:strip="">
+
+  <py:def function="page_title">Administration Dashboard</py:def>
+  <py:def function="page_heading">Administration Dashboard</py:def>
+
+  <div py:match="content">
+    <h3>Current Sysadmins</h3>
+    <p>You can change sysadmins on the <a
+      href="${h.url_for('ckanadmin',
+      action='authz')}">authorization page</a>.</p>
+    <ul>
+      <li py:for="user in c.sysadmins">
+        ${h.linked_user(user)}
+      </li>
+    </ul>
+  </div>
+
+  <xi:include href="layout.html" />
+</html>
+


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/admin/layout.html	Mon Oct 10 12:14:06 2011 +0100
@@ -0,0 +1,30 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml"
+  xmlns:py="http://genshi.edgewall.org/"
+  xmlns:xi="http://www.w3.org/2001/XInclude"
+  py:strip=""
+  >
+  <py:match path="minornavigation">
+    <ul class="tabbed">
+      <li py:attrs="{'class':'current-tab'} if c.action=='index' else {}">
+        <a href="${h.url_for('ckanadmin', action='index')}">
+          Home
+        </a>
+      </li>
+      <li py:attrs="{'class':'current-tab'} if c.action=='authz' else {}">
+        <a href="${h.url_for('ckanadmin', action='authz')}">
+          Authorization
+        </a>
+      </li>
+      <li py:attrs="{'class':'current-tab'} if c.action=='trash' else {}">
+        <a href="${h.url_for('ckanadmin', action='trash')}">
+          Trash
+        </a>
+      </li>
+    </ul>
+  </py:match>
+  
+  <xi:include href="../layout.html" />
+</html>
+
+


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/templates/admin/trash.html	Mon Oct 10 12:14:06 2011 +0100
@@ -0,0 +1,48 @@
+<html xmlns:py="http://genshi.edgewall.org/"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:xi="http://www.w3.org/2001/XInclude"
+  py:strip="">
+
+  <py:def function="page_title">Administration - Trash</py:def>
+  <py:def function="page_heading">Administration - Trash</py:def>
+
+  <div py:match="content">
+    <h3>Deleted Revisions</h3>
+    ${revision_list(c.deleted_revisions)}
+    <form method="POST" id="form-purge-revisions">
+      <button
+        type="submit"
+        name="purge-revisions"
+        value="purge"
+        py:attrs=
+          "{'disabled':'disabled','class':'pretty-button'} 
+          if not any(c.deleted_revisions) 
+          else {'class':'pretty-button danger'}"
+        >
+        Purge them all (forever and irreversibly)
+      </button>
+      <div class="clear"></div>
+    </form>
+
+    <h3>Deleted Datasets</h3>
+    ${package_list(c.deleted_packages)}
+    <span py:if="not any(c.deleted_packages)"><em>(None)</em></span>
+    <form method="POST" id="form-purge-packages">
+      <button
+        type="submit"
+        name="purge-packages"
+        value="purge"
+        py:attrs=
+          "{'disabled':'disabled','class':'pretty-button'} 
+          if not any(c.deleted_packages) 
+          else {'class':'pretty-button danger'}"
+        >
+        Purge them all (forever and irreversibly)
+      </button>
+      <div class="clear"></div>
+    </form>
+  </div>
+
+  <xi:include href="layout.html" />
+</html>
+


--- a/ckan/templates/layout_base.html	Mon Oct 10 11:36:03 2011 +0300
+++ b/ckan/templates/layout_base.html	Mon Oct 10 12:14:06 2011 +0100
@@ -157,7 +157,7 @@
                 </a></li><li>
-                <a href="${url('/ckan-admin')}">
+                <a href="${h.url_for('ckanadmin_index')}">
                   Site Admin
                 </a></li>


--- a/ckan/templates/revision/read.html	Mon Oct 10 11:36:03 2011 +0300
+++ b/ckan/templates/revision/read.html	Mon Oct 10 12:14:06 2011 +0100
@@ -17,10 +17,10 @@
               id=c.revision.id)}"
           ><py:if test="c.revision.state!='deleted'">
-          <button type="submit" name="action" value="delete">Delete</button>
+          <button type="submit" name="action" value="delete" class="pretty-button">Delete</button></py:if><py:if test="c.revision.state=='deleted'">
-          <button type="submit" name="action" value="undelete">Undelete</button>
+          <button type="submit" name="action" value="undelete" class="pretty-button">Undelete</button></py:if></form></div>


--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/ckan/tests/functional/test_admin.py	Mon Oct 10 12:14:06 2011 +0100
@@ -0,0 +1,359 @@
+import os
+from paste.deploy import appconfig
+import paste.fixture
+from ckan.config.middleware import make_app
+import ckan.model as model
+from ckan.tests import conf_dir, url_for, CreateTestData
+from controllers.admin import get_sysadmins
+
+class TestAdminController:
+    @classmethod
+    def setup_class(cls):
+        config = appconfig('config:test.ini', relative_to=conf_dir)
+        wsgiapp = make_app(config.global_conf, **config.local_conf)
+        cls.app = paste.fixture.TestApp(wsgiapp)
+        # setup test data including testsysadmin user
+        CreateTestData.create()
+
+    @classmethod
+    def teardown_class(self):
+        CreateTestData.delete()
+
+    #test that only sysadmins can access the /ckan-admin page
+    def test_index(self):
+        url = url_for('ckanadmin', action='index')
+        # redirect as not authorized
+        response = self.app.get(url, status=[302])
+        # random username
+        response = self.app.get(url, status=[401],
+                extra_environ={'REMOTE_USER': 'my-random-user-name'})
+        # now test real access
+        username = u'testsysadmin'.encode('utf8')
+        response = self.app.get(url,
+                extra_environ={'REMOTE_USER': username})
+        assert 'Administration' in response, response
+
+
+class TestAdminAuthzController:
+    @classmethod
+    def setup_class(cls):
+        config = appconfig('config:test.ini', relative_to=conf_dir)
+        wsgiapp = make_app(config.global_conf, **config.local_conf)
+        cls.app = paste.fixture.TestApp(wsgiapp)
+        # setup test data including testsysadmin user
+        CreateTestData.create()
+        # Creating a couple of authorization groups, which are enough to break
+        # some things just by their existence
+        for ag_name in [u'anauthzgroup', u'anotherauthzgroup']:
+            ag=model.AuthorizationGroup.by_name(ag_name) 
+            if not ag: #may already exist, if not create
+                ag=model.AuthorizationGroup(name=ag_name)
+                model.Session.add(ag)
+        model.Session.commit()
+        #they are especially dangerous if they have a role on the System
+        ag = model.AuthorizationGroup.by_name(u'anauthzgroup')
+        model.add_authorization_group_to_role(ag, u'editor', model.System())
+        model.Session.commit()
+
+    @classmethod
+    def teardown_class(self):
+        CreateTestData.delete()
+
+    def test_role_table(self):
+
+        #logged in as testsysadmin for all actions
+        as_testsysadmin = {'REMOTE_USER': 'testsysadmin'}
+
+        def get_system_user_roles():
+            sys_query=model.Session.query(model.SystemRole)
+            return sorted([(x.user.name,x.role) for x in sys_query.all() if x.user])
+
+        def get_system_authzgroup_roles():
+            sys_query=model.Session.query(model.SystemRole)
+            return sorted([(x.authorized_group.name,x.role) for x in sys_query.all() if x.authorized_group])
+
+        def get_response():
+            response = self.app.get(
+                    url_for('ckanadmin', action='authz'),
+                    extra_environ=as_testsysadmin)
+            assert 'Administration - Authorization' in response, response
+            return response
+
+        def get_user_form():
+           response = get_response()
+           return response.forms['theform']
+
+        def get_authzgroup_form():
+           response = get_response()
+           return response.forms['authzgroup_form']
+
+        def check_and_set_checkbox(theform, user, role, should_be, set_to):
+           user_role_string = '%s$%s' % (user, role)
+           checkboxes = [x for x in theform.fields[user_role_string] \
+                                           if x.__class__.__name__ == 'Checkbox']
+
+           assert(len(checkboxes)==1), \
+                "there should only be one checkbox for %s/%s" % (user, role)
+           checkbox = checkboxes[0]
+
+           #checkbox should be unticked
+           assert checkbox.checked==should_be, \
+                         "%s/%s checkbox in unexpected state" % (user, role)
+
+           #tick or untick the box and submit the form
+           checkbox.checked=set_to
+           return theform
+
+        def submit(form):
+          return form.submit('save', extra_environ=as_testsysadmin)
+
+        def authz_submit(form):
+          return form.submit('authz_save', extra_environ=as_testsysadmin)
+            
+        # get and store the starting state of the system roles
+        original_user_roles = get_system_user_roles()
+        original_authzgroup_roles = get_system_authzgroup_roles()
+
+        # also keep a copy that we can update as the tests go on
+        expected_user_roles = get_system_user_roles()
+        expected_authzgroup_roles = get_system_authzgroup_roles()
+
+        # before we start changing things, check that the roles on the system are as expected
+        assert original_user_roles == \
+            [(u'logged_in', u'editor'), (u'testsysadmin', u'admin'),  (u'visitor', u'anon_editor')] , \
+            "original user roles not as expected " + str(original_user_roles)
+
+        assert original_authzgroup_roles == [(u'anauthzgroup', u'editor')], \
+            "original authzgroup roles not as expected" + str(original_authzgroup_roles)
+
+
+        # visitor is not an admin. check that his admin box is unticked, tick it, and submit
+        submit(check_and_set_checkbox(get_user_form(), u'visitor', u'admin', False, True))
+
+        # update expected state to reflect the change we should just have made
+        expected_user_roles.append((u'visitor', u'admin'))
+        expected_user_roles.sort()
+
+        # and check that's the state in the database now
+        assert get_system_user_roles() == expected_user_roles
+        assert get_system_authzgroup_roles() == expected_authzgroup_roles
+
+        # try again, this time we expect the box to be ticked already
+        submit(check_and_set_checkbox(get_user_form(), u'visitor', u'admin', True, True))
+
+        # performing the action twice shouldn't have changed anything
+        assert get_system_user_roles() == expected_user_roles
+        assert get_system_authzgroup_roles() == expected_authzgroup_roles
+
+        # now let's make the authzgroup which already has a system role an admin
+        authz_submit(check_and_set_checkbox(get_authzgroup_form(), u'anauthzgroup', u'admin', False, True))
+
+        # update expected state to reflect the change we should just have made
+        expected_authzgroup_roles.append((u'anauthzgroup', u'admin'))
+        expected_authzgroup_roles.sort()
+
+        # check that's happened
+        assert get_system_user_roles() == expected_user_roles
+        assert get_system_authzgroup_roles() == expected_authzgroup_roles
+
+        # put it back how it was
+        submit(check_and_set_checkbox(get_user_form(), u'visitor', u'admin', True, False))
+        authz_submit(check_and_set_checkbox(get_authzgroup_form(), u'anauthzgroup', u'admin', True, False))
+
+        # should be back to our starting state
+        assert original_user_roles == get_system_user_roles()
+        assert original_authzgroup_roles == get_system_authzgroup_roles()
+
+
+        # now test making multiple changes
+
+
+        # change lots of things
+        form = get_user_form()
+        check_and_set_checkbox(form, u'visitor', u'editor', False, True)
+        check_and_set_checkbox(form, u'visitor', u'reader', False,  False)
+        check_and_set_checkbox(form, u'logged_in', u'editor', True, False)
+        check_and_set_checkbox(form, u'logged_in', u'reader', False, True)      
+        submit(form)
+
+        roles=get_system_user_roles()
+        # and assert that they've actually changed
+        assert (u'visitor', u'editor') in roles and \
+               (u'logged_in', u'editor') not in roles and \
+               (u'logged_in', u'reader') in roles and \
+               (u'visitor', u'reader')  not in roles, \
+               "visitor and logged_in roles seem not to have reversed"
+
+
+        def get_roles_by_name(user=None, group=None):
+            if user:
+                return [y for (x,y) in get_system_user_roles() if x==user]
+            elif group:
+                return [y for (x,y) in get_system_authzgroup_roles() if x==group]
+            else: 
+                assert False, 'miscalled'
+
+
+        # now we test the box for giving roles to an arbitrary user
+
+        # check that tester doesn't have a system role
+        assert len(get_roles_by_name(user=u'tester'))==0, \
+              "tester should not have roles"
+
+        # get the put tester in the username box
+        form = get_response().forms['addform']
+        form.fields['new_user_name'][0].value='tester'
+        # get the admin checkbox
+        checkbox = [x for x in form.fields['admin'] \
+                      if x.__class__.__name__ == 'Checkbox'][0]
+        # check it's currently unticked
+        assert checkbox.checked == False
+        # tick it and submit
+        checkbox.checked=True
+        response = form.submit('add', extra_environ=as_testsysadmin)
+        assert "User Added" in response, "don't see flash message"
+
+        assert get_roles_by_name(user=u'tester') == ['admin'], \
+            "tester should be an admin now"
+
+        # and similarly for an arbitrary authz group
+        assert get_roles_by_name(group=u'anotherauthzgroup') == [], \
+           "should not have roles"
+
+        form = get_response().forms['authzgroup_addform']
+        form.fields['new_user_name'][0].value='anotherauthzgroup'
+        checkbox = [x for x in form.fields['reader'] \
+                        if x.__class__.__name__ == 'Checkbox'][0]
+        assert checkbox.checked == False
+        checkbox.checked=True
+        
+        response = form.submit('authz_add', extra_environ=as_testsysadmin)
+        assert "Authorization Group Added" in response, "don't see flash message"
+
+
+        assert get_roles_by_name(group=u'anotherauthzgroup') == [u'reader'], \
+               "should be a reader now"
+
+
+class TestAdminTrashController:
+    def setup(cls):
+        config = appconfig('config:test.ini', relative_to=conf_dir)
+        wsgiapp = make_app(config.global_conf, **config.local_conf)
+        cls.app = paste.fixture.TestApp(wsgiapp)
+        CreateTestData.create()
+
+    def teardown(self):
+        model.repo.rebuild_db()
+
+    def test_purge_revision(self):
+        as_testsysadmin = {'REMOTE_USER': 'testsysadmin'}
+
+        # Put a revision in deleted state
+        rev = model.repo.youngest_revision()
+        revid = rev.id
+        rev.state = model.State.DELETED
+        model.Session.commit()
+
+        # check it shows up on trash page and
+        url = url_for('ckanadmin', action='trash')
+        response = self.app.get(url, extra_environ=as_testsysadmin)
+        assert revid in response, response
+
+        # check it can be successfully purged
+        form = response.forms['form-purge-revisions']
+        res = form.submit('purge-revisions', status=[302], extra_environ=as_testsysadmin)
+        res = res.follow(extra_environ=as_testsysadmin)
+        assert not revid in res, res
+        rev = model.Session.query(model.Revision).filter_by(id=revid).first()
+        assert rev is None, rev
+
+    def test_purge_package(self):
+        as_testsysadmin = {'REMOTE_USER': 'testsysadmin'}
+
+        # Put packages in deleted state
+        rev = model.repo.new_revision()
+        pkg = model.Package.by_name(u'warandpeace')
+        pkg.state = model.State.DELETED
+        model.repo.commit_and_remove()
+
+        # Check shows up on trash page
+        url = url_for('ckanadmin', action='trash')
+        response = self.app.get(url, extra_environ=as_testsysadmin)
+        assert 'dataset/warandpeace' in response, response
+        
+        # Check we get correct error message on attempted purge
+        form = response.forms['form-purge-packages']
+        response = form.submit('purge-packages', status=[302],
+                extra_environ=as_testsysadmin)
+        response = response.follow(extra_environ=as_testsysadmin)
+        assert 'Cannot purge package' in response, response
+        assert 'dataset/warandpeace' in response
+
+        # now check we really can purge when things are ok
+        pkg = model.Package.by_name(u'annakarenina')
+        pkg.state = model.State.DELETED
+        model.repo.new_revision()
+        model.Session.commit()
+
+        response = self.app.get(url, extra_environ=as_testsysadmin)
+        assert 'dataset/warandpeace' in response, response
+        assert 'dataset/annakarenina' in response, response
+
+        form = response.forms['form-purge-packages']
+        res = form.submit('purge-packages', status=[302], extra_environ=as_testsysadmin)
+        res = res.follow(extra_environ=as_testsysadmin)
+
+        pkgs = model.Session.query(model.Package).all()
+        assert len(pkgs) == 0
+
+    def test_purge_youngest_revision(self):
+        as_testsysadmin = {'REMOTE_USER': 'testsysadmin'}
+
+        id = u'warandpeace'
+        log_message = 'test_1234'
+        edit_url = url_for(controller='package', action='edit', id=id)
+
+        # Manually create a revision
+        res = self.app.get(edit_url)
+        fv = res.forms['dataset-edit']
+        fv['title'] = 'RevisedTitle'
+        fv['log_message'] = log_message
+        res = fv.submit('save')
+
+        # Delete that revision
+        rev = model.repo.youngest_revision()
+        assert rev.message == log_message
+        rev.state = model.State.DELETED
+        model.Session.commit()
+
+        # Run a purge
+        url = url_for('ckanadmin', action='trash')
+        res = self.app.get(url, extra_environ=as_testsysadmin)
+        form = res.forms['form-purge-revisions']
+        res = form.submit('purge-revisions', status=[302], extra_environ=as_testsysadmin)
+        res = res.follow(extra_environ=as_testsysadmin)
+
+        # Verify the edit page can be loaded (ie. does not 404)
+        res = self.app.get(edit_url)
+
+    def test_undelete(self):
+        as_testsysadmin = {'REMOTE_USER': 'testsysadmin'}
+
+        rev = model.repo.youngest_revision()
+        rev_id = rev.id
+        rev.state = model.State.DELETED
+        model.Session.commit()
+
+        # Click undelete
+        url = url_for('ckanadmin', action='trash')
+        res = self.app.get(url, extra_environ=as_testsysadmin)
+        form = res.forms['undelete-'+rev.id]
+        res = form.submit('submit', status=[302], extra_environ=as_testsysadmin)
+        res = res.follow(extra_environ=as_testsysadmin)
+
+        assert 'Revision updated' in res
+        assert not 'DELETED' in res
+
+        rev = model.repo.youngest_revision()
+        assert rev.id == rev_id
+        assert rev.state == model.State.ACTIVE


--- a/doc/configuration.rst	Mon Oct 10 11:36:03 2011 +0300
+++ b/doc/configuration.rst	Mon Oct 10 12:14:06 2011 +0100
@@ -472,7 +472,7 @@
 
 Example::
 
-  ckan.plugins = disqus synchronous_search datapreview googleanalytics stats storage admin follower
+  ckan.plugins = disqus synchronous_search datapreview googleanalytics stats storage follower
 
 Specify which CKAN extensions are to be enabled. 
 


--- a/doc/extensions.rst	Mon Oct 10 11:36:03 2011 +0300
+++ b/doc/extensions.rst	Mon Oct 10 12:14:06 2011 +0100
@@ -15,7 +15,6 @@
 
 Some popular extensions include: 
 
-* `ckanext-admin <https://bitbucket.org/okfn/ckanext-admin>`_: Admin web interface for CKAN.
 * `ckanext-apps <https://bitbucket.org/okfn/ckanext-apps>`_: Apps and ideas catalogue extension for CKAN.
 * `ckanext-deliverance <https://bitbucket.org/okfn/ckanext-deliverance>`_: Extends CKAN to use the Deliverance HTTP proxy, which can request and render web pages from * an external site (e.g. a CMS like Drupal or Wordpress). 
 * `ckanext-disqus <https://bitbucket.org/okfn/ckanext-disqus>`_: Allows users to comment on dataset pages with Disqus.

Repository URL: https://bitbucket.org/okfn/ckan/

--

This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.




More information about the ckan-changes mailing list