From c3b9df6da4d6899dde8b410c5e6a07e8cb8d8605 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 18 Jan 2012 16:48:44 +0000 Subject: [PATCH 01/59] Initial commit, just creating the module --- ckan/logic/auth/publisher/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ckan/logic/auth/publisher/__init__.py diff --git a/ckan/logic/auth/publisher/__init__.py b/ckan/logic/auth/publisher/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 30f99b607a18be3f5e2ad9aa2731e1a06a778bae Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 18 Jan 2012 17:26:24 +0000 Subject: [PATCH 02/59] Changed new_authz to also load the publisher specific auth from the logic.auth.publisher module --- ckan/logic/auth/publisher/create.py | 3 +++ ckan/logic/auth/publisher/delete.py | 0 ckan/logic/auth/publisher/get.py | 0 ckan/logic/auth/publisher/update.py | 0 ckan/new_authz.py | 31 +++++++++++++++++++---------- 5 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 ckan/logic/auth/publisher/create.py create mode 100644 ckan/logic/auth/publisher/delete.py create mode 100644 ckan/logic/auth/publisher/get.py create mode 100644 ckan/logic/auth/publisher/update.py diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py new file mode 100644 index 00000000000..afdf916decb --- /dev/null +++ b/ckan/logic/auth/publisher/create.py @@ -0,0 +1,3 @@ + +# Make sure all functions defined here have publisher_ in front of their name +# so they are differentiated from the original auth functions. \ No newline at end of file diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ckan/new_authz.py b/ckan/new_authz.py index 46c6b8b4c36..a98b8d952f3 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -25,19 +25,28 @@ def _get_auth_function(action): # Rather than writing them out in full will use __import__ # to load anything from ckan.auth that looks like it might # be an authorisation function + + # These lambdas are used to describe how we modify the name of the + modules = [ + 'ckan.logic.auth', + 'ckan.logic.auth.publisher' + ] + for auth_module_name in ['get', 'create', 'update','delete']: - module_path = 'ckan.logic.auth.'+auth_module_name - try: - module = __import__(module_path) - except ImportError,e: - log.debug('No auth module for action "%s"' % auth_module_name) - continue + for modroot in module_keys.keys(): + module_path = modroot + '.' + auth_module_name + try: + module = __import__(module_path) + except ImportError,e: + log.debug('No auth module for action "%s"' % auth_module_name) + continue - for part in module_path.split('.')[1:]: - module = getattr(module, part) - for k, v in module.__dict__.items(): - if not k.startswith('_'): - _auth_functions[k] = v + for part in module_path.split('.')[1:]: + module = getattr(module, part) + + for key, v in module.__dict__.items(): + if not key.startswith('_'): + _auth_functions[key] = v # Then overwrite them with any specific ones in the plugins: resolved_auth_function_plugins = {} From c1a69f3efc87760eefb39b91efc723e0dccbc1b6 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Thu, 19 Jan 2012 09:22:14 +0000 Subject: [PATCH 03/59] Initial outline of the publisher auth --- ckan/logic/auth/publisher/create.py | 134 ++++++++++++++++++++- ckan/logic/auth/publisher/delete.py | 59 +++++++++ ckan/logic/auth/publisher/get.py | 174 +++++++++++++++++++++++++++ ckan/logic/auth/publisher/update.py | 178 ++++++++++++++++++++++++++++ ckan/new_authz.py | 5 + 5 files changed, 548 insertions(+), 2 deletions(-) diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index afdf916decb..5db4fbde9fa 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -1,3 +1,133 @@ +# Updated: False -# Make sure all functions defined here have publisher_ in front of their name -# so they are differentiated from the original auth functions. \ No newline at end of file +from ckan.logic import check_access_old, NotFound +from ckan.authz import Authorizer +from ckan.lib.base import _ + + +def publisher_package_create(context, data_dict=None): + model = context['model'] + user = context['user'] + + check1 = check_access_old(model.System(), model.Action.PACKAGE_CREATE, context) + + if not check1: + return {'success': False, 'msg': _('User %s not authorized to create packages') % str(user)} + else: + + check2 = check_group_auth(context,data_dict) + if not check2: + return {'success': False, 'msg': _('User %s not authorized to edit these groups') % str(user)} + + return {'success': True} + +def publisher_resource_create(context, data_dict): + return {'success': False, 'msg': 'Not implemented yet in the auth refactor'} + +def publisher_package_relationship_create(context, data_dict): + model = context['model'] + user = context['user'] + + id = data_dict['id'] + id2 = data_dict['id2'] + pkg1 = model.Package.get(id) + pkg2 = model.Package.get(id2) + + authorized = Authorizer().\ + authorized_package_relationship(\ + user, pkg1, pkg2, action=model.Action.EDIT) + + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to edit these packages') % str(user)} + else: + return {'success': True} + +def publisher_group_create(context, data_dict=None): + model = context['model'] + user = context['user'] + + authorized = check_access_old(model.System(), model.Action.GROUP_CREATE, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to create groups') % str(user)} + else: + return {'success': True} + +def publisher_authorization_group_create(context, data_dict=None): + model = context['model'] + user = context['user'] + + authorized = check_access_old(model.System(), model.Action.AUTHZ_GROUP_CREATE, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to create authorization groups') % str(user)} + else: + return {'success': True} + +def publisher_rating_create(context, data_dict): + # No authz check in the logic function + return {'success': True} + +def publisher_user_create(context, data_dict=None): + model = context['model'] + user = context['user'] + + authorized = check_access_old(model.System(), model.Action.USER_CREATE, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to create users') % str(user)} + else: + return {'success': True} + +def publisher_check_group_auth(context, data_dict): + if not data_dict: + return True + + model = context['model'] + pkg = context.get("package") + + api_version = context.get('api_version') or '1' + + group_blobs = data_dict.get("groups", []) + groups = set() + for group_blob in group_blobs: + # group_blob might be a dict or a group_ref + if isinstance(group_blob, dict): + if api_version == '1': + id = group_blob.get('name') + else: + id = group_blob.get('id') + if not id: + continue + else: + id = group_blob + grp = model.Group.get(id) + if grp is None: + raise NotFound(_('Group was not found.')) + groups.add(grp) + + if pkg: + pkg_groups = pkg.get_groups() + + groups = groups - set(pkg_groups) + + for group in groups: + if not check_access_old(group, model.Action.EDIT, context): + return False + + return True + +## Modifications for rest api + +def publisher_package_create_rest(context, data_dict): + model = context['model'] + user = context['user'] + if user in (model.PSEUDO_USER__VISITOR, ''): + return {'success': False, 'msg': _('Valid API key needed to create a package')} + + return package_create(context, data_dict) + +def publisher_group_create_rest(context, data_dict): + model = context['model'] + user = context['user'] + if user in (model.PSEUDO_USER__VISITOR, ''): + return {'success': False, 'msg': _('Valid API key needed to create a group')} + + return group_create(context, data_dict) diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index e69de29bb2d..990f04d55cb 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -0,0 +1,59 @@ +# Updated: False + +from ckan.logic import check_access_old +from ckan.logic.auth import get_package_object, get_group_object +from ckan.logic.auth.create import package_relationship_create +from ckan.authz import Authorizer +from ckan.lib.base import _ + +def publisher_package_delete(context, data_dict): + model = context['model'] + user = context['user'] + package = get_package_object(context, data_dict) + + authorized = check_access_old(package, model.Action.PURGE, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to delete package %s') % (str(user),package.id)} + else: + return {'success': True} + +def publisher_package_relationship_delete(context, data_dict): + return package_relationship_create(context, data_dict) + +def publisher_relationship_delete(context, data_dict): + model = context['model'] + user = context['user'] + relationship = context['relationship'] + + authorized = check_access_old(relationship, model.Action.PURGE, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to delete relationship %s') % (str(user),relationship.id)} + else: + return {'success': True} + +def publisher_group_delete(context, data_dict): + model = context['model'] + user = context['user'] + group = get_group_object(context, data_dict) + + authorized = check_access_old(group, model.Action.PURGE, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to delete group %s') % (str(user),group.id)} + else: + return {'success': True} + +def publisher_revision_undelete(context, data_dict): + return {'success': False, 'msg': 'Not implemented yet in the auth refactor'} + +def publisher_revision_delete(context, data_dict): + return {'success': False, 'msg': 'Not implemented yet in the auth refactor'} + +def publisher_task_status_delete(context, data_dict): + model = context['model'] + user = context['user'] + + authorized = Authorizer().is_sysadmin(unicode(user)) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to delete task_status') % str(user)} + else: + return {'success': True} diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index e69de29bb2d..766bfea72bd 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -0,0 +1,174 @@ +# Updated: False + +from ckan.logic import check_access_old, NotFound +from ckan.authz import Authorizer +from ckan.lib.base import _ +from ckan.logic.auth import get_package_object, get_group_object, get_resource_object + + +def publisher_site_read(context, data_dict): + """\ + This function should be deprecated. It is only here because we couldn't + get hold of Friedrich to ask what it was for. + + ./ckan/controllers/api.py + """ + model = context['model'] + user = context.get('user') + if not Authorizer().is_authorized(user, model.Action.SITE_READ, model.System): + return {'success': False, 'msg': _('Not authorized to see this page')} + + return {'success': True} + +def publisher_package_search(context, data_dict): + # Everyone can search by default + return {'success': True} + +def publisher_package_list(context, data_dict): + # List of all active packages are visible by default + return {'success': True} + +def publisher_current_package_list_with_resources(context, data_dict): + return package_list(context, data_dict) + +def publisher_revision_list(context, data_dict): + # In our new model everyone can read the revison list + return {'success': True} + +def publisher_group_revision_list(context, data_dict): + return group_show(context, data_dict) + +def publisher_package_revision_list(context, data_dict): + return package_show(context, data_dict) + +def publisher_group_list(context, data_dict): + # List of all active groups is visible by default + return {'success': True} + +def publisher_group_list_authz(context, data_dict): + return group_list(context, data_dict) + +def publisher_group_list_available(context, data_dict): + return group_list(context, data_dict) + +def publisher_licence_list(context, data_dict): + # Licences list is visible by default + return {'success': True} + +def publisher_tag_list(context, data_dict): + # Tags list is visible by default + return {'success': True} + +def publisher_user_list(context, data_dict): + # Users list is visible by default + return {'success': True} + +def publisher_package_relationships_list(context, data_dict): + model = context['model'] + user = context.get('user') + + id = data_dict['id'] + id2 = data_dict.get('id2') + pkg1 = model.Package.get(id) + pkg2 = model.Package.get(id2) + + authorized = Authorizer().\ + authorized_package_relationship(\ + user, pkg1, pkg2, action=model.Action.READ) + + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to read these packages') % str(user)} + else: + return {'success': True} + +def publisher_package_show(context, data_dict): + model = context['model'] + user = context.get('user') + package = get_package_object(context, data_dict) + + authorized = check_access_old(package, model.Action.READ, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} + else: + return {'success': True} + +def publisher_resource_show(context, data_dict): + model = context['model'] + user = context.get('user') + resource = get_resource_object(context, data_dict) + + # check authentication against package + query = model.Session.query(model.Package)\ + .join(model.ResourceGroup)\ + .join(model.Resource)\ + .filter(model.ResourceGroup.id == resource.resource_group_id) + pkg = query.first() + if not pkg: + raise NotFound(_('No package found for this resource, cannot check auth.')) + + pkg_dict = {'id': pkg.id} + authorized = package_show(context, pkg_dict).get('success') + + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (str(user), resource.id)} + else: + return {'success': True} + +def publisher_revision_show(context, data_dict): + # No authz check in the logic function + return {'success': True} + +def publisher_group_show(context, data_dict): + model = context['model'] + user = context.get('user') + group = get_group_object(context, data_dict) + + authorized = check_access_old(group, model.Action.READ, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to read group %s') % (str(user),group.id)} + else: + return {'success': True} + +def publisher_tag_show(context, data_dict): + # No authz check in the logic function + return {'success': True} + +def publisher_user_show(context, data_dict): + # By default, user details can be read by anyone, but some properties like + # the API key are stripped at the action level if not not logged in. + return {'success': True} + +def publisher_package_autocomplete(context, data_dict): + return publisher_package_list(context, data_dict) + +def publisher_group_autocomplete(context, data_dict): + return publisher_group_list(context, data_dict) + +def publisher_tag_autocomplete(context, data_dict): + return publisher_tag_list(context, data_dict) + +def publisher_user_autocomplete(context, data_dict): + return publisher_user_list(context, data_dict) + +def publisher_format_autocomplete(context, data_dict): + return {'success': True} + +def publisher_task_status_show(context, data_dict): + return {'success': True} + +## Modifications for rest api + +def publisher_package_show_rest(context, data_dict): + return publisher_package_show(context, data_dict) + +def publisher_group_show_rest(context, data_dict): + return publisher_group_show(context, data_dict) + +def publisher_tag_show_rest(context, data_dict): + return publisher_tag_show(context, data_dict) + +def publisher_get_site_user(context, data_dict): + if not context.get('ignore_auth'): + return {'success': False, 'msg': 'Only internal services allowed to use this action'} + else: + return {'success': True} diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index e69de29bb2d..246b247e5b7 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -0,0 +1,178 @@ +# Updated: False + +from ckan.logic import check_access_old, NotFound +from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ + get_user_object, get_resource_object +from ckan.logic.auth.create import check_group_auth, package_relationship_create +from ckan.authz import Authorizer +from ckan.lib.base import _ + +def publisher_make_latest_pending_package_active(context, data_dict): + return publisher_package_update(context, data_dict) + +def publisher_package_update(context, data_dict): + model = context['model'] + user = context.get('user') + package = get_package_object(context, data_dict) + + check1 = check_access_old(package, model.Action.EDIT, context) + if not check1: + return {'success': False, 'msg': _('User %s not authorized to edit package %s') % (str(user), package.id)} + else: + check2 = check_group_auth(context,data_dict) + if not check2: + return {'success': False, 'msg': _('User %s not authorized to edit these groups') % str(user)} + + return {'success': True} + +def publisher_resource_update(context, data_dict): + model = context['model'] + user = context.get('user') + resource = get_resource_object(context, data_dict) + + # check authentication against package + query = model.Session.query(model.Package)\ + .join(model.ResourceGroup)\ + .join(model.Resource)\ + .filter(model.ResourceGroup.id == resource.resource_group_id) + pkg = query.first() + if not pkg: + raise NotFound(_('No package found for this resource, cannot check auth.')) + + pkg_dict = {'id': pkg.id} + authorized = package_update(context, pkg_dict).get('success') + + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to read edit %s') % (str(user), resource.id)} + else: + return {'success': True} + +def publisher_package_relationship_update(context, data_dict): + return package_relationship_create(context, data_dict) + +def publisher_package_change_state(context, data_dict): + model = context['model'] + user = context['user'] + package = get_package_object(context, data_dict) + + authorized = check_access_old(package, model.Action.CHANGE_STATE, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to change state of package %s') % (str(user),package.id)} + else: + return {'success': True} + +def publisher_package_edit_permissions(context, data_dict): + model = context['model'] + user = context['user'] + package = get_package_object(context, data_dict) + + authorized = check_access_old(package, model.Action.EDIT_PERMISSIONS, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to edit permissions of package %s') % (str(user),package.id)} + else: + return {'success': True} + +def publisher_group_update(context, data_dict): + model = context['model'] + user = context['user'] + group = get_group_object(context, data_dict) + + authorized = check_access_old(group, model.Action.EDIT, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to edit group %s') % (str(user),group.id)} + else: + return {'success': True} + +def publisher_group_change_state(context, data_dict): + model = context['model'] + user = context['user'] + group = get_group_object(context, data_dict) + + authorized = check_access_old(group, model.Action.CHANGE_STATE, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to change state of group %s') % (str(user),group.id)} + else: + return {'success': True} + +def publisher_group_edit_permissions(context, data_dict): + model = context['model'] + user = context['user'] + group = get_group_object(context, data_dict) + + authorized = check_access_old(group, model.Action.EDIT_PERMISSIONS, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to edit permissions of group %s') % (str(user),group.id)} + else: + return {'success': True} + +def publisher_authorization_group_update(context, data_dict): + model = context['model'] + user = context['user'] + authorization_group = get_authorization_group_object(context, data_dict) + + authorized = check_access_old(authorization_group, model.Action.EDIT, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to edit permissions of authorization group %s') % (str(user),authorization_group.id)} + else: + return {'success': True} + +def publisher_authorization_group_edit_permissions(context, data_dict): + model = context['model'] + user = context['user'] + authorization_group = get_authorization_group_object(context, data_dict) + + authorized = check_access_old(authorization_group, model.Action.EDIT_PERMISSIONS, context) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to edit permissions of authorization group %s') % (str(user),authorization_group.id)} + else: + return {'success': True} + +def publisher_user_update(context, data_dict): + model = context['model'] + user = context['user'] + user_obj = get_user_object(context, data_dict) + + if not (Authorizer().is_sysadmin(unicode(user)) or user == user_obj.name) and \ + not ('reset_key' in data_dict and data_dict['reset_key'] == user_obj.reset_key): + return {'success': False, 'msg': _('User %s not authorized to edit user %s') % (str(user), user_obj.id)} + + return {'success': True} + +def publisher_revision_change_state(context, data_dict): + model = context['model'] + user = context['user'] + + authorized = Authorizer().is_authorized(user, model.Action.CHANGE_STATE, model.Revision) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to change state of revision' ) % str(user)} + else: + return {'success': True} + +def publisher_task_status_update(context, data_dict): + model = context['model'] + user = context['user'] + + authorized = Authorizer().is_sysadmin(unicode(user)) + if not authorized: + return {'success': False, 'msg': _('User %s not authorized to update task_status table') % str(user)} + else: + return {'success': True} + +## Modifications for rest api + +def publisher_package_update_rest(context, data_dict): + model = context['model'] + user = context['user'] + if user in (model.PSEUDO_USER__VISITOR, ''): + return {'success': False, 'msg': _('Valid API key needed to edit a package')} + + return publisher_package_update(context, data_dict) + +def publisher_group_update_rest(context, data_dict): + model = context['model'] + user = context['user'] + if user in (model.PSEUDO_USER__VISITOR, ''): + return {'success': False, 'msg': _('Valid API key needed to edit a group')} + + return publisher_group_update(context, data_dict) + diff --git a/ckan/new_authz.py b/ckan/new_authz.py index a98b8d952f3..1c4c55cb45c 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -11,6 +11,11 @@ _auth_functions = {} def is_authorized(action, context,data_dict=None): + + # Ideally if we can find out if this is a publisher user (i.e. is part + # of a publisher) then we can add publisher_ to the front of the action + # name to use the publisher auth instead of the standard one. + auth_function = _get_auth_function(action) if auth_function: return auth_function(context, data_dict) From e17769b7ef4d0decfa352f85d66353a60a46fcf2 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Thu, 19 Jan 2012 16:13:43 +0000 Subject: [PATCH 04/59] Allowed new_authz to change authorisation profile based on configuration and updated docs to reflect the change. Publisher profile is currently the same as the default profile --- ckan/logic/auth/publisher/create.py | 20 ++++---- ckan/logic/auth/publisher/delete.py | 14 +++--- ckan/logic/auth/publisher/get.py | 74 ++++++++++++++--------------- ckan/logic/auth/publisher/update.py | 38 +++++++-------- ckan/new_authz.py | 42 +++++++++------- doc/configuration.rst | 15 +++++- 6 files changed, 111 insertions(+), 92 deletions(-) diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index 5db4fbde9fa..ac34f639b03 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -5,7 +5,7 @@ from ckan.lib.base import _ -def publisher_package_create(context, data_dict=None): +def package_create(context, data_dict=None): model = context['model'] user = context['user'] @@ -21,10 +21,10 @@ def publisher_package_create(context, data_dict=None): return {'success': True} -def publisher_resource_create(context, data_dict): +def resource_create(context, data_dict): return {'success': False, 'msg': 'Not implemented yet in the auth refactor'} -def publisher_package_relationship_create(context, data_dict): +def package_relationship_create(context, data_dict): model = context['model'] user = context['user'] @@ -42,7 +42,7 @@ def publisher_package_relationship_create(context, data_dict): else: return {'success': True} -def publisher_group_create(context, data_dict=None): +def group_create(context, data_dict=None): model = context['model'] user = context['user'] @@ -52,7 +52,7 @@ def publisher_group_create(context, data_dict=None): else: return {'success': True} -def publisher_authorization_group_create(context, data_dict=None): +def authorization_group_create(context, data_dict=None): model = context['model'] user = context['user'] @@ -62,11 +62,11 @@ def publisher_authorization_group_create(context, data_dict=None): else: return {'success': True} -def publisher_rating_create(context, data_dict): +def rating_create(context, data_dict): # No authz check in the logic function return {'success': True} -def publisher_user_create(context, data_dict=None): +def user_create(context, data_dict=None): model = context['model'] user = context['user'] @@ -76,7 +76,7 @@ def publisher_user_create(context, data_dict=None): else: return {'success': True} -def publisher_check_group_auth(context, data_dict): +def check_group_auth(context, data_dict): if not data_dict: return True @@ -116,7 +116,7 @@ def publisher_check_group_auth(context, data_dict): ## Modifications for rest api -def publisher_package_create_rest(context, data_dict): +def package_create_rest(context, data_dict): model = context['model'] user = context['user'] if user in (model.PSEUDO_USER__VISITOR, ''): @@ -124,7 +124,7 @@ def publisher_package_create_rest(context, data_dict): return package_create(context, data_dict) -def publisher_group_create_rest(context, data_dict): +def group_create_rest(context, data_dict): model = context['model'] user = context['user'] if user in (model.PSEUDO_USER__VISITOR, ''): diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index 990f04d55cb..f4cb8cda4c9 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -6,7 +6,7 @@ from ckan.authz import Authorizer from ckan.lib.base import _ -def publisher_package_delete(context, data_dict): +def package_delete(context, data_dict): model = context['model'] user = context['user'] package = get_package_object(context, data_dict) @@ -17,10 +17,10 @@ def publisher_package_delete(context, data_dict): else: return {'success': True} -def publisher_package_relationship_delete(context, data_dict): +def package_relationship_delete(context, data_dict): return package_relationship_create(context, data_dict) -def publisher_relationship_delete(context, data_dict): +def relationship_delete(context, data_dict): model = context['model'] user = context['user'] relationship = context['relationship'] @@ -31,7 +31,7 @@ def publisher_relationship_delete(context, data_dict): else: return {'success': True} -def publisher_group_delete(context, data_dict): +def group_delete(context, data_dict): model = context['model'] user = context['user'] group = get_group_object(context, data_dict) @@ -42,13 +42,13 @@ def publisher_group_delete(context, data_dict): else: return {'success': True} -def publisher_revision_undelete(context, data_dict): +def revision_undelete(context, data_dict): return {'success': False, 'msg': 'Not implemented yet in the auth refactor'} -def publisher_revision_delete(context, data_dict): +def revision_delete(context, data_dict): return {'success': False, 'msg': 'Not implemented yet in the auth refactor'} -def publisher_task_status_delete(context, data_dict): +def task_status_delete(context, data_dict): model = context['model'] user = context['user'] diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index 766bfea72bd..43ac403d924 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -6,7 +6,7 @@ from ckan.logic.auth import get_package_object, get_group_object, get_resource_object -def publisher_site_read(context, data_dict): +def site_read(context, data_dict): """\ This function should be deprecated. It is only here because we couldn't get hold of Friedrich to ask what it was for. @@ -20,50 +20,50 @@ def publisher_site_read(context, data_dict): return {'success': True} -def publisher_package_search(context, data_dict): +def package_search(context, data_dict): # Everyone can search by default return {'success': True} -def publisher_package_list(context, data_dict): +def package_list(context, data_dict): # List of all active packages are visible by default return {'success': True} -def publisher_current_package_list_with_resources(context, data_dict): +def current_package_list_with_resources(context, data_dict): return package_list(context, data_dict) -def publisher_revision_list(context, data_dict): +def revision_list(context, data_dict): # In our new model everyone can read the revison list return {'success': True} -def publisher_group_revision_list(context, data_dict): +def group_revision_list(context, data_dict): return group_show(context, data_dict) -def publisher_package_revision_list(context, data_dict): +def package_revision_list(context, data_dict): return package_show(context, data_dict) -def publisher_group_list(context, data_dict): +def group_list(context, data_dict): # List of all active groups is visible by default return {'success': True} -def publisher_group_list_authz(context, data_dict): +def group_list_authz(context, data_dict): return group_list(context, data_dict) -def publisher_group_list_available(context, data_dict): +def group_list_available(context, data_dict): return group_list(context, data_dict) -def publisher_licence_list(context, data_dict): +def licence_list(context, data_dict): # Licences list is visible by default return {'success': True} -def publisher_tag_list(context, data_dict): +def tag_list(context, data_dict): # Tags list is visible by default return {'success': True} -def publisher_user_list(context, data_dict): +def user_list(context, data_dict): # Users list is visible by default return {'success': True} -def publisher_package_relationships_list(context, data_dict): +def package_relationships_list(context, data_dict): model = context['model'] user = context.get('user') @@ -81,7 +81,7 @@ def publisher_package_relationships_list(context, data_dict): else: return {'success': True} -def publisher_package_show(context, data_dict): +def package_show(context, data_dict): model = context['model'] user = context.get('user') package = get_package_object(context, data_dict) @@ -92,7 +92,7 @@ def publisher_package_show(context, data_dict): else: return {'success': True} -def publisher_resource_show(context, data_dict): +def resource_show(context, data_dict): model = context['model'] user = context.get('user') resource = get_resource_object(context, data_dict) @@ -114,11 +114,11 @@ def publisher_resource_show(context, data_dict): else: return {'success': True} -def publisher_revision_show(context, data_dict): +def revision_show(context, data_dict): # No authz check in the logic function return {'success': True} -def publisher_group_show(context, data_dict): +def group_show(context, data_dict): model = context['model'] user = context.get('user') group = get_group_object(context, data_dict) @@ -129,45 +129,45 @@ def publisher_group_show(context, data_dict): else: return {'success': True} -def publisher_tag_show(context, data_dict): +def tag_show(context, data_dict): # No authz check in the logic function return {'success': True} -def publisher_user_show(context, data_dict): +def user_show(context, data_dict): # By default, user details can be read by anyone, but some properties like # the API key are stripped at the action level if not not logged in. return {'success': True} -def publisher_package_autocomplete(context, data_dict): - return publisher_package_list(context, data_dict) +def package_autocomplete(context, data_dict): + return package_list(context, data_dict) -def publisher_group_autocomplete(context, data_dict): - return publisher_group_list(context, data_dict) +def group_autocomplete(context, data_dict): + return group_list(context, data_dict) -def publisher_tag_autocomplete(context, data_dict): - return publisher_tag_list(context, data_dict) +def tag_autocomplete(context, data_dict): + return tag_list(context, data_dict) -def publisher_user_autocomplete(context, data_dict): - return publisher_user_list(context, data_dict) +def user_autocomplete(context, data_dict): + return user_list(context, data_dict) -def publisher_format_autocomplete(context, data_dict): +def format_autocomplete(context, data_dict): return {'success': True} -def publisher_task_status_show(context, data_dict): +def task_status_show(context, data_dict): return {'success': True} ## Modifications for rest api -def publisher_package_show_rest(context, data_dict): - return publisher_package_show(context, data_dict) +def package_show_rest(context, data_dict): + return package_show(context, data_dict) -def publisher_group_show_rest(context, data_dict): - return publisher_group_show(context, data_dict) +def group_show_rest(context, data_dict): + return group_show(context, data_dict) -def publisher_tag_show_rest(context, data_dict): - return publisher_tag_show(context, data_dict) +def tag_show_rest(context, data_dict): + return tag_show(context, data_dict) -def publisher_get_site_user(context, data_dict): +def get_site_user(context, data_dict): if not context.get('ignore_auth'): return {'success': False, 'msg': 'Only internal services allowed to use this action'} else: diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 246b247e5b7..9c8164b8dc2 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -7,10 +7,10 @@ from ckan.authz import Authorizer from ckan.lib.base import _ -def publisher_make_latest_pending_package_active(context, data_dict): - return publisher_package_update(context, data_dict) +def make_latest_pending_package_active(context, data_dict): + return package_update(context, data_dict) -def publisher_package_update(context, data_dict): +def package_update(context, data_dict): model = context['model'] user = context.get('user') package = get_package_object(context, data_dict) @@ -25,7 +25,7 @@ def publisher_package_update(context, data_dict): return {'success': True} -def publisher_resource_update(context, data_dict): +def resource_update(context, data_dict): model = context['model'] user = context.get('user') resource = get_resource_object(context, data_dict) @@ -47,10 +47,10 @@ def publisher_resource_update(context, data_dict): else: return {'success': True} -def publisher_package_relationship_update(context, data_dict): +def package_relationship_update(context, data_dict): return package_relationship_create(context, data_dict) -def publisher_package_change_state(context, data_dict): +def package_change_state(context, data_dict): model = context['model'] user = context['user'] package = get_package_object(context, data_dict) @@ -61,7 +61,7 @@ def publisher_package_change_state(context, data_dict): else: return {'success': True} -def publisher_package_edit_permissions(context, data_dict): +def package_edit_permissions(context, data_dict): model = context['model'] user = context['user'] package = get_package_object(context, data_dict) @@ -72,7 +72,7 @@ def publisher_package_edit_permissions(context, data_dict): else: return {'success': True} -def publisher_group_update(context, data_dict): +def group_update(context, data_dict): model = context['model'] user = context['user'] group = get_group_object(context, data_dict) @@ -83,7 +83,7 @@ def publisher_group_update(context, data_dict): else: return {'success': True} -def publisher_group_change_state(context, data_dict): +def group_change_state(context, data_dict): model = context['model'] user = context['user'] group = get_group_object(context, data_dict) @@ -94,7 +94,7 @@ def publisher_group_change_state(context, data_dict): else: return {'success': True} -def publisher_group_edit_permissions(context, data_dict): +def group_edit_permissions(context, data_dict): model = context['model'] user = context['user'] group = get_group_object(context, data_dict) @@ -105,7 +105,7 @@ def publisher_group_edit_permissions(context, data_dict): else: return {'success': True} -def publisher_authorization_group_update(context, data_dict): +def authorization_group_update(context, data_dict): model = context['model'] user = context['user'] authorization_group = get_authorization_group_object(context, data_dict) @@ -116,7 +116,7 @@ def publisher_authorization_group_update(context, data_dict): else: return {'success': True} -def publisher_authorization_group_edit_permissions(context, data_dict): +def authorization_group_edit_permissions(context, data_dict): model = context['model'] user = context['user'] authorization_group = get_authorization_group_object(context, data_dict) @@ -127,7 +127,7 @@ def publisher_authorization_group_edit_permissions(context, data_dict): else: return {'success': True} -def publisher_user_update(context, data_dict): +def user_update(context, data_dict): model = context['model'] user = context['user'] user_obj = get_user_object(context, data_dict) @@ -138,7 +138,7 @@ def publisher_user_update(context, data_dict): return {'success': True} -def publisher_revision_change_state(context, data_dict): +def revision_change_state(context, data_dict): model = context['model'] user = context['user'] @@ -148,7 +148,7 @@ def publisher_revision_change_state(context, data_dict): else: return {'success': True} -def publisher_task_status_update(context, data_dict): +def task_status_update(context, data_dict): model = context['model'] user = context['user'] @@ -160,19 +160,19 @@ def publisher_task_status_update(context, data_dict): ## Modifications for rest api -def publisher_package_update_rest(context, data_dict): +def package_update_rest(context, data_dict): model = context['model'] user = context['user'] if user in (model.PSEUDO_USER__VISITOR, ''): return {'success': False, 'msg': _('Valid API key needed to edit a package')} - return publisher_package_update(context, data_dict) + return package_update(context, data_dict) -def publisher_group_update_rest(context, data_dict): +def group_update_rest(context, data_dict): model = context['model'] user = context['user'] if user in (model.PSEUDO_USER__VISITOR, ''): return {'success': False, 'msg': _('Valid API key needed to edit a group')} - return publisher_group_update(context, data_dict) + return group_update(context, data_dict) diff --git a/ckan/new_authz.py b/ckan/new_authz.py index 1c4c55cb45c..3e2ce70d261 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -23,35 +23,41 @@ def is_authorized(action, context,data_dict=None): raise ValueError(_('Authorization function not found: %s' % action)) def _get_auth_function(action): + from pylons import config + if _auth_functions: return _auth_functions.get(action) + # Otherwise look in all the plugins to resolve all possible # First get the default ones in the ckan/logic/auth directory # Rather than writing them out in full will use __import__ # to load anything from ckan.auth that looks like it might # be an authorisation function - # These lambdas are used to describe how we modify the name of the - modules = [ - 'ckan.logic.auth', - 'ckan.logic.auth.publisher' - ] + # We will load the auth profile from settings + module_root = 'ckan.logic.auth' + auth_profile = config.get('auth.profile', '') + if auth_profile: + module_root = '%s.%s' % (module_root, auth_profile) + + log.info('Using auth profile at %s' % module_root) for auth_module_name in ['get', 'create', 'update','delete']: - for modroot in module_keys.keys(): - module_path = modroot + '.' + auth_module_name - try: - module = __import__(module_path) - except ImportError,e: - log.debug('No auth module for action "%s"' % auth_module_name) - continue + module_path = '%s.%s' % (module_root, auth_module_name,) + try: + module = __import__(module_path) + log.info('Loaded module %r' % module) + except ImportError,e: + print 'Failed to find auth module' + log.debug('No auth module for action "%s"' % auth_module_name) + continue - for part in module_path.split('.')[1:]: - module = getattr(module, part) - - for key, v in module.__dict__.items(): - if not key.startswith('_'): - _auth_functions[key] = v + for part in module_path.split('.')[1:]: + module = getattr(module, part) + + for key, v in module.__dict__.items(): + if not key.startswith('_'): + _auth_functions[key] = v # Then overwrite them with any specific ones in the plugins: resolved_auth_function_plugins = {} diff --git a/doc/configuration.rst b/doc/configuration.rst index fb29c220d42..71756db831c 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -532,7 +532,7 @@ Authorization Settings ---------------------- .. index:: - single: default_roles + single: default_roles, auth_profile default_roles ^^^^^^^^^^^^^ @@ -551,6 +551,19 @@ With this example setting, visitors and logged-in users can only read datasets t Defaults: see in ``ckan/model/authz.py`` for: ``default_default_user_roles`` +auth_profile +^^^^^^^^^^^^ + +This allows you to specify the auth profile to use for this installation. By default this is empty and uses the default authorisation code, if set to publisher it will use the publisher profile in ckan/logic/auth/publisher. + +Example:: + auth.profile = publisher + +With this example setting the publisher auth profile will be used. + +Defaults: The default authorisation from ``ckan/logic/auth/*`` will be used + + Plugin Settings --------------- From 06bb3dc530a864259296985e219d3d3c20cc8237 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 20 Jan 2012 09:34:38 +0000 Subject: [PATCH 05/59] Test that the publisher profile has the same number of auth functions as the default profile --- ckan/logic/auth/publisher/get.py | 1 + ckan/tests/misc/test_auth_profiles.py | 41 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 ckan/tests/misc/test_auth_profiles.py diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index 43ac403d924..4c49de2c9a1 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -15,6 +15,7 @@ def site_read(context, data_dict): """ model = context['model'] user = context.get('user') + if not Authorizer().is_authorized(user, model.Action.SITE_READ, model.System): return {'success': False, 'msg': _('Not authorized to see this page')} diff --git a/ckan/tests/misc/test_auth_profiles.py b/ckan/tests/misc/test_auth_profiles.py new file mode 100644 index 00000000000..06a2d4c2ca4 --- /dev/null +++ b/ckan/tests/misc/test_auth_profiles.py @@ -0,0 +1,41 @@ +from ckan.tests import * +import ckan.forms +import ckan.model as model +from ckan.lib.create_test_data import CreateTestData +from ckan.lib.package_saver import PackageSaver +from ckan.tests.pylons_controller import PylonsTestCase + +class TestAuthProfiles(PylonsTestCase): + + @classmethod + def setup_class(self): + model.repo.init_db() + + @classmethod + def teardown_class(self): + model.repo.rebuild_db() + + def test_authorizer_count(self): + """ Ensure that we have the same number of auth functions in the + core auth profile as in the publisher auth profile """ + + modules = { + 'ckan.logic.auth': 0, + 'ckan.logic.auth.publisher': 0 + } + + for module_root in modules.keys(): + print module_root + for auth_module_name in ['get', 'create', 'update','delete']: + module_path = '%s.%s' % (module_root, auth_module_name,) + module = __import__(module_path) + + for part in module_path.split('.')[1:]: + module = getattr(module, part) + + for key, v in module.__dict__.items(): + if not key.startswith('_'): + modules[module_root] = modules[module_root] + 1 + + assert modules['ckan.logic.auth'] == modules['ckan.logic.auth.publisher'] + From 4f0c86e2a791f9d5a5fd9db2a39e512281f43efc Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 20 Jan 2012 11:25:14 +0000 Subject: [PATCH 06/59] Test whether loading the profile works as expected, are we loading the correct module --- ckan/new_authz.py | 13 +++---------- ckan/tests/misc/test_auth_profiles.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/ckan/new_authz.py b/ckan/new_authz.py index 3e2ce70d261..55728be207d 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -11,11 +11,6 @@ _auth_functions = {} def is_authorized(action, context,data_dict=None): - - # Ideally if we can find out if this is a publisher user (i.e. is part - # of a publisher) then we can add publisher_ to the front of the action - # name to use the publisher auth instead of the standard one. - auth_function = _get_auth_function(action) if auth_function: return auth_function(context, data_dict) @@ -27,7 +22,7 @@ def _get_auth_function(action): if _auth_functions: return _auth_functions.get(action) - + # Otherwise look in all the plugins to resolve all possible # First get the default ones in the ckan/logic/auth directory # Rather than writing them out in full will use __import__ @@ -36,7 +31,7 @@ def _get_auth_function(action): # We will load the auth profile from settings module_root = 'ckan.logic.auth' - auth_profile = config.get('auth.profile', '') + auth_profile = config.get('ckan.auth.profile', '') if auth_profile: module_root = '%s.%s' % (module_root, auth_profile) @@ -46,15 +41,13 @@ def _get_auth_function(action): module_path = '%s.%s' % (module_root, auth_module_name,) try: module = __import__(module_path) - log.info('Loaded module %r' % module) except ImportError,e: - print 'Failed to find auth module' log.debug('No auth module for action "%s"' % auth_module_name) continue for part in module_path.split('.')[1:]: module = getattr(module, part) - + for key, v in module.__dict__.items(): if not key.startswith('_'): _auth_functions[key] = v diff --git a/ckan/tests/misc/test_auth_profiles.py b/ckan/tests/misc/test_auth_profiles.py index 06a2d4c2ca4..269bdf3060f 100644 --- a/ckan/tests/misc/test_auth_profiles.py +++ b/ckan/tests/misc/test_auth_profiles.py @@ -1,10 +1,10 @@ from ckan.tests import * import ckan.forms import ckan.model as model -from ckan.lib.create_test_data import CreateTestData -from ckan.lib.package_saver import PackageSaver from ckan.tests.pylons_controller import PylonsTestCase +from pylons import config + class TestAuthProfiles(PylonsTestCase): @classmethod @@ -15,6 +15,16 @@ def setup_class(self): def teardown_class(self): model.repo.rebuild_db() + def test_load_publisher_profile(self): + """ Ensure that the relevant config settings result in the appropriate + functions being loaded from the correct module """ + from new_authz import is_authorized, _get_auth_function + + config['ckan.auth.profile'] = 'publisher' + _ = is_authorized('site_read', {'model': model, 'user': '127.0.0.1','reset_auth_profile':True}) + s = str(_get_auth_function('site_read').__module__) + assert s == 'ckan.logic.auth.publisher.get', s + def test_authorizer_count(self): """ Ensure that we have the same number of auth functions in the core auth profile as in the publisher auth profile """ @@ -25,7 +35,6 @@ def test_authorizer_count(self): } for module_root in modules.keys(): - print module_root for auth_module_name in ['get', 'create', 'update','delete']: module_path = '%s.%s' % (module_root, auth_module_name,) module = __import__(module_path) From bacd0b8c56453794ba15cfc4112fae6f01cc8320 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 20 Jan 2012 11:44:19 +0000 Subject: [PATCH 07/59] Imports of helper functions --- ckan/logic/auth/create.py | 1 - ckan/logic/auth/publisher/create.py | 4 +++- ckan/logic/auth/publisher/delete.py | 3 ++- ckan/logic/auth/publisher/get.py | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py index 80702bf176f..7ba1b489703 100644 --- a/ckan/logic/auth/create.py +++ b/ckan/logic/auth/create.py @@ -6,7 +6,6 @@ def package_create(context, data_dict=None): model = context['model'] user = context['user'] - check1 = check_access_old(model.System(), model.Action.PACKAGE_CREATE, context) if not check1: diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index ac34f639b03..ce840b496d7 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -1,5 +1,7 @@ # Updated: False +from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ + get_user_object, get_resource_object from ckan.logic import check_access_old, NotFound from ckan.authz import Authorizer from ckan.lib.base import _ @@ -8,7 +10,7 @@ def package_create(context, data_dict=None): model = context['model'] user = context['user'] - + check1 = check_access_old(model.System(), model.Action.PACKAGE_CREATE, context) if not check1: diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index f4cb8cda4c9..cc9de97b9ba 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -1,5 +1,6 @@ # Updated: False - +from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ + get_user_object, get_resource_object from ckan.logic import check_access_old from ckan.logic.auth import get_package_object, get_group_object from ckan.logic.auth.create import package_relationship_create diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index 4c49de2c9a1..95f8b188254 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -1,5 +1,6 @@ # Updated: False - +from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ + get_user_object, get_resource_object from ckan.logic import check_access_old, NotFound from ckan.authz import Authorizer from ckan.lib.base import _ From ba417cd2502583a9dcec71282160cff9cc1bde08 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 23 Jan 2012 12:18:07 +0000 Subject: [PATCH 08/59] Added helper function --- ckan/model/group.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ckan/model/group.py b/ckan/model/group.py index 2fc52d3ac9d..fd591e3ccc1 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -50,6 +50,15 @@ def __init__(self, group=None, table_id=None, group_id=None, self.capacity = capacity self.state = state + def get_related(self, type): + """ TODO: Determine if this is useful + Get all objects that are members of the group of the specified type. + + Should the type be used to get table_name or should we use the one in + the constructor + """ + pass + def related_packages(self): # TODO do we want to return all related packages or certain ones? return Session.query(Package).filter_by(id=self.table_id).all() From 69cba2dd68ed75e5df4e59f7b8d0b5486053333a Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 23 Jan 2012 14:57:35 +0000 Subject: [PATCH 09/59] Changes to the models that are expected to be 'members' of groups to have get_groups() and first pass at publisher auth profile. Checks in auth/publisher/update.py use _groups_intersect() to determine whether the user's groups intersect with those of the object being edited, and if so then it allows the flow to continue --- ckan/controllers/group.py | 2 +- ckan/logic/auth/publisher/__init__.py | 7 +++++ ckan/logic/auth/publisher/create.py | 5 ++++ ckan/logic/auth/publisher/update.py | 42 +++++++++++++++++++++++++-- ckan/model/group.py | 33 +++++++++++++++++++-- ckan/model/package.py | 4 +++ ckan/model/user.py | 14 +++++++++ 7 files changed, 102 insertions(+), 5 deletions(-) diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 4af61cc2e97..c4e7fe3f2d9 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -31,7 +31,7 @@ def _db_to_form_schema(self): def _setup_template_variables(self, context): c.is_sysadmin = Authorizer().is_sysadmin(c.user) - + ## This is messy as auths take domain object not data_dict context_group = context.get('group',None) group = context_group or c.group diff --git a/ckan/logic/auth/publisher/__init__.py b/ckan/logic/auth/publisher/__init__.py index e69de29bb2d..83ff06105ea 100644 --- a/ckan/logic/auth/publisher/__init__.py +++ b/ckan/logic/auth/publisher/__init__.py @@ -0,0 +1,7 @@ + + +def _groups_intersect( groups_A, groups_B ): + """ Return true if any of the groups in A are also in B (or size + of intersection > 0)""" + return len( set( groups_A ).intersection( set(groups_B) ) ) > 0 + \ No newline at end of file diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index ce840b496d7..cfeaa6a5f5e 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -2,6 +2,7 @@ from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ get_user_object, get_resource_object +from ckan.logic.auth.publisher import _groups_intersect from ckan.logic import check_access_old, NotFound from ckan.authz import Authorizer from ckan.lib.base import _ @@ -48,6 +49,8 @@ def group_create(context, data_dict=None): model = context['model'] user = context['user'] + # TODO: We need to check whether this group is being created within another group + authorized = check_access_old(model.System(), model.Action.GROUP_CREATE, context) if not authorized: return {'success': False, 'msg': _('User %s not authorized to create groups') % str(user)} @@ -57,6 +60,8 @@ def group_create(context, data_dict=None): def authorization_group_create(context, data_dict=None): model = context['model'] user = context['user'] + + # TODO: We need to check whether this group is being created within another group authorized = check_access_old(model.System(), model.Action.AUTHZ_GROUP_CREATE, context) if not authorized: diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 9c8164b8dc2..6c13a30437a 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -1,8 +1,7 @@ -# Updated: False - from ckan.logic import check_access_old, NotFound from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ get_user_object, get_resource_object +from ckan.logic.auth.publisher import _groups_intersect from ckan.logic.auth.create import check_group_auth, package_relationship_create from ckan.authz import Authorizer from ckan.lib.base import _ @@ -15,6 +14,12 @@ def package_update(context, data_dict): user = context.get('user') package = get_package_object(context, data_dict) + userobj = model.User.get( user ) + + # Only allow package update if the user and package groups intersect + if not _groups_intersect( userobj.get_groups(), package.get_groups() ): + return {'success': False, 'msg': _('User %s not authorized to edit packages in these groups') % str(user)} + check1 = check_access_old(package, model.Action.EDIT, context) if not check1: return {'success': False, 'msg': _('User %s not authorized to edit package %s') % (str(user), package.id)} @@ -30,6 +35,11 @@ def resource_update(context, data_dict): user = context.get('user') resource = get_resource_object(context, data_dict) + # Only allow resource update if the user and resource packages groups intersect + userobj = model.User.get( user ) + if not _groups_intersect( userobj.get_groups(), resource.package.get_groups() ): + return {'success': False, 'msg': _('User %s not authorized to edit resources in this package') % str(user)} + # check authentication against package query = model.Session.query(model.Package)\ .join(model.ResourceGroup)\ @@ -55,6 +65,10 @@ def package_change_state(context, data_dict): user = context['user'] package = get_package_object(context, data_dict) + userobj = model.User.get( user ) + if not _groups_intersect( userobj.get_groups(), package.get_groups() ): + return {'success': False, 'msg': _('User %s not authorized to change this package state') % str(user)} + authorized = check_access_old(package, model.Action.CHANGE_STATE, context) if not authorized: return {'success': False, 'msg': _('User %s not authorized to change state of package %s') % (str(user),package.id)} @@ -66,6 +80,11 @@ def package_edit_permissions(context, data_dict): user = context['user'] package = get_package_object(context, data_dict) + # Only allow package update if the user and package groups intersect + userobj = model.User.get( user ) + if not _groups_intersect( userobj.get_groups(), package.get_groups() ): + return {'success': False, 'msg': _('User %s not authorized to edit permissions of this package') % str(user)} + authorized = check_access_old(package, model.Action.EDIT_PERMISSIONS, context) if not authorized: return {'success': False, 'msg': _('User %s not authorized to edit permissions of package %s') % (str(user),package.id)} @@ -77,6 +96,11 @@ def group_update(context, data_dict): user = context['user'] group = get_group_object(context, data_dict) + # Only allow package update if the user and package groups intersect + userobj = model.User.get( user ) + if not _groups_intersect( userobj.get_groups(), group.get_groups() ): + return {'success': False, 'msg': _('User %s not authorized to edit this group') % str(user)} + authorized = check_access_old(group, model.Action.EDIT, context) if not authorized: return {'success': False, 'msg': _('User %s not authorized to edit group %s') % (str(user),group.id)} @@ -88,6 +112,10 @@ def group_change_state(context, data_dict): user = context['user'] group = get_group_object(context, data_dict) + userobj = model.User.get( user ) + if not _groups_intersect( userobj.get_groups(), group.get_groups() ): + return {'success': False, 'msg': _('User %s not authorized to change state of group') % str(user)} + authorized = check_access_old(group, model.Action.CHANGE_STATE, context) if not authorized: return {'success': False, 'msg': _('User %s not authorized to change state of group %s') % (str(user),group.id)} @@ -99,6 +127,11 @@ def group_edit_permissions(context, data_dict): user = context['user'] group = get_group_object(context, data_dict) + # Only allow package update if the user and package groups intersect + userobj = model.User.get( user ) + if not _groups_intersect( userobj.get_groups(), group.get_groups() ): + return {'success': False, 'msg': _('User %s not authorized to edit permissions of group') % str(user)} + authorized = check_access_old(group, model.Action.EDIT_PERMISSIONS, context) if not authorized: return {'success': False, 'msg': _('User %s not authorized to edit permissions of group %s') % (str(user),group.id)} @@ -136,6 +169,11 @@ def user_update(context, data_dict): not ('reset_key' in data_dict and data_dict['reset_key'] == user_obj.reset_key): return {'success': False, 'msg': _('User %s not authorized to edit user %s') % (str(user), user_obj.id)} + # Only allow package update if the user and package groups intersect or user is editing self + if (user != user_obj.name) and + not _groups_intersect( current_user.get_groups(), user_obj.get_groups() ): + return {'success': False, 'msg': _('User %s not authorized to edit user') % str(user)} + return {'success': True} def revision_change_state(context, data_dict): diff --git a/ckan/model/group.py b/ckan/model/group.py index fd591e3ccc1..784ea2f38e0 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -89,6 +89,24 @@ def get(cls, reference): return group # Todo: Make sure group names can't be changed to look like group IDs? + def members_of_type(self, object_type): + object_type_string = object_type.__name__.lower() + query = Session.query(object_type).\ + filter_by(state=vdm.sqlalchemy.State.ACTIVE).\ + filter(group_table.c.id == self.id).\ + filter(member_table.c.state == 'active').\ + filter(member_table.c.table_name == object_type_string).\ + join(member_table, member_table.c.table_id == getattr(object_type,'id') ).\ + join(group_table, group_table.c.id == member_table.c.group_id) + return query + + def add_child(self, object_instance): + object_type_string = object_instance.__class__.__name__.lower() + if not object_instance in self.members_of_type(object_instance.__class__).all(): + member = Member(group=self, table_id=getattr(object_instance,'id'), table_name=object_type_string) + Session.add(member) + + def active_packages(self, load_eager=True): query = Session.query(Package).\ filter_by(state=vdm.sqlalchemy.State.ACTIVE).\ @@ -114,10 +132,22 @@ def add_package_by_name(self, package_name): return package = Package.by_name(package_name) assert package - if not package in self.active_packages().all(): + if not package in self.members_of_type( package.__class__ ).all(): member = Member(group=self, table_id=package.id, table_name='package') Session.add(member) + def get_groups(self): + """ Get all groups that this group is within """ + import ckan.model as model + if '_groups' not in self.__dict__: + self._groups = model.Session.query(model.Group).\ + join(model.Member, model.Member.group_id == model.Group.id).\ + join(model.Group, model.Group.id == model.Member.table_id).\ + join(model.Package, model.Member.table_name == 'group' ).\ + filter(model.Member.state == 'active').\ + filter(model.Group.id == self.id).all() + return self._groups + @property def all_related_revisions(self): @@ -170,4 +200,3 @@ def __repr__(self): #TODO MemberRevision.related_packages = lambda self: [self.continuity.package] - diff --git a/ckan/model/package.py b/ckan/model/package.py index 9f5e072133e..81e98f49f57 100644 --- a/ckan/model/package.py +++ b/ckan/model/package.py @@ -498,12 +498,16 @@ def metadata_modified(self): timestamp_float = timegm(timestamp_without_usecs) + usecs return datetime.datetime.utcfromtimestamp(timestamp_float) + def is_in_group(self, group): + return group in self.get_groups() + def get_groups(self): import ckan.model as model if '_groups' not in self.__dict__: self._groups = model.Session.query(model.Group).\ join(model.Member, model.Member.group_id == model.Group.id).\ join(model.Package, model.Package.id == model.Member.table_id).\ + join(model.Package, model.Member.table_name == 'package' ).\ filter(model.Member.state == 'active').\ filter(model.Package.id == self.id).all() return self._groups diff --git a/ckan/model/user.py b/ckan/model/user.py index bf3e33e74b8..04e388fa675 100644 --- a/ckan/model/user.py +++ b/ckan/model/user.py @@ -145,6 +145,20 @@ def number_administered_packages(self): q = q.filter_by(user=self, role=model.Role.ADMIN) return q.count() + def is_in_group(self, group): + return group in self.get_groups() + + def get_groups(self): + import ckan.model as model + if '_groups' not in self.__dict__: + self._groups = model.Session.query(model.Group).\ + join(model.Member, model.Member.group_id == model.Group.id).\ + join(model.User, model.User.id == model.Member.table_id).\ + join(model.Package, model.Member.table_name == 'user' ).\ + filter(model.Member.state == 'active').\ + filter(model.User.id == self.id).all() + return self._groups + @classmethod def search(cls, querystr, sqlalchemy_query=None): '''Search name, fullname, email and openid. From 0a1aceff0c85cedfdeb1d234cd826899c3c7ca31 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 23 Jan 2012 15:06:48 +0000 Subject: [PATCH 10/59] Fixed syntax problem --- ckan/logic/auth/publisher/update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 6c13a30437a..8b56e6c4780 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -170,8 +170,8 @@ def user_update(context, data_dict): return {'success': False, 'msg': _('User %s not authorized to edit user %s') % (str(user), user_obj.id)} # Only allow package update if the user and package groups intersect or user is editing self - if (user != user_obj.name) and - not _groups_intersect( current_user.get_groups(), user_obj.get_groups() ): + if (user != user_obj.name) and \ + not _groups_intersect( current_user.get_groups(), user_obj.get_groups() ): return {'success': False, 'msg': _('User %s not authorized to edit user') % str(user)} return {'success': True} From 24495e5293bc3a90df9bc9a72e9bac42dc975906 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 23 Jan 2012 15:42:35 +0000 Subject: [PATCH 11/59] Changes to fix the bad impl of get_groups(). Tests now successfully fail --- ckan/logic/auth/publisher/__init__.py | 12 ++++++++++-- ckan/logic/auth/publisher/update.py | 1 + ckan/model/group.py | 7 +++---- ckan/model/package.py | 6 +++--- ckan/model/user.py | 6 +++--- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/ckan/logic/auth/publisher/__init__.py b/ckan/logic/auth/publisher/__init__.py index 83ff06105ea..d04a024b261 100644 --- a/ckan/logic/auth/publisher/__init__.py +++ b/ckan/logic/auth/publisher/__init__.py @@ -2,6 +2,14 @@ def _groups_intersect( groups_A, groups_B ): """ Return true if any of the groups in A are also in B (or size - of intersection > 0)""" - return len( set( groups_A ).intersection( set(groups_B) ) ) > 0 + of intersection > 0). If both are empty for now we will allow it """ + # TODO: Fix me. + + ga = set(groups_A) + gb = set(groups_B) + + if len(gb) + len(ga) == 0: + return True + + return len( ga.intersection( gb ) ) > 0 \ No newline at end of file diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 8b56e6c4780..a912ac5a5bd 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -201,6 +201,7 @@ def task_status_update(context, data_dict): def package_update_rest(context, data_dict): model = context['model'] user = context['user'] + if user in (model.PSEUDO_USER__VISITOR, ''): return {'success': False, 'msg': _('Valid API key needed to edit a package')} diff --git a/ckan/model/group.py b/ckan/model/group.py index 784ea2f38e0..8c20c23ae16 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -141,11 +141,10 @@ def get_groups(self): import ckan.model as model if '_groups' not in self.__dict__: self._groups = model.Session.query(model.Group).\ - join(model.Member, model.Member.group_id == model.Group.id).\ - join(model.Group, model.Group.id == model.Member.table_id).\ - join(model.Package, model.Member.table_name == 'group' ).\ + join(model.Member, model.Member.group_id == self.id and \ + model.Member.table_name == 'group').\ filter(model.Member.state == 'active').\ - filter(model.Group.id == self.id).all() + filter(model.Member.table_id == self.id).all() return self._groups diff --git a/ckan/model/package.py b/ckan/model/package.py index 81e98f49f57..c39ea0d6990 100644 --- a/ckan/model/package.py +++ b/ckan/model/package.py @@ -505,11 +505,11 @@ def get_groups(self): import ckan.model as model if '_groups' not in self.__dict__: self._groups = model.Session.query(model.Group).\ - join(model.Member, model.Member.group_id == model.Group.id).\ + join(model.Member, model.Member.group_id == model.Group.id and \ + model.Member.table_name == 'package' ).\ join(model.Package, model.Package.id == model.Member.table_id).\ - join(model.Package, model.Member.table_name == 'package' ).\ filter(model.Member.state == 'active').\ - filter(model.Package.id == self.id).all() + filter(model.Member.table_id == self.id).all() return self._groups @property diff --git a/ckan/model/user.py b/ckan/model/user.py index 04e388fa675..04c9e45d8eb 100644 --- a/ckan/model/user.py +++ b/ckan/model/user.py @@ -152,11 +152,11 @@ def get_groups(self): import ckan.model as model if '_groups' not in self.__dict__: self._groups = model.Session.query(model.Group).\ - join(model.Member, model.Member.group_id == model.Group.id).\ + join(model.Member, model.Member.group_id == model.Group.id and\ + model.Member.table_name == 'user' ).\ join(model.User, model.User.id == model.Member.table_id).\ - join(model.Package, model.Member.table_name == 'user' ).\ filter(model.Member.state == 'active').\ - filter(model.User.id == self.id).all() + filter(model.Member.table_id == self.id).all() return self._groups @classmethod From 02ec14d7ef2cdcbe4cf0ef5ae1e1996b1c540a20 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 23 Jan 2012 17:00:26 +0000 Subject: [PATCH 12/59] Cleaning up the ordering of the group checks so that it does not cause issues --- ckan/logic/auth/publisher/update.py | 40 +++++++++++++++++------------ ckan/model/group.py | 8 ++++-- ckan/model/package.py | 7 +++-- ckan/model/user.py | 8 ++++-- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index a912ac5a5bd..55b108828d3 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -14,12 +14,7 @@ def package_update(context, data_dict): user = context.get('user') package = get_package_object(context, data_dict) - userobj = model.User.get( user ) - # Only allow package update if the user and package groups intersect - if not _groups_intersect( userobj.get_groups(), package.get_groups() ): - return {'success': False, 'msg': _('User %s not authorized to edit packages in these groups') % str(user)} - check1 = check_access_old(package, model.Action.EDIT, context) if not check1: return {'success': False, 'msg': _('User %s not authorized to edit package %s') % (str(user), package.id)} @@ -28,6 +23,13 @@ def package_update(context, data_dict): if not check2: return {'success': False, 'msg': _('User %s not authorized to edit these groups') % str(user)} + userobj = model.User.get( user ) + if not userobj or \ + not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): + return {'success': False, + 'msg': _('User %s not authorized to edit packages in these groups') % str(user)} + + return {'success': True} def resource_update(context, data_dict): @@ -37,7 +39,7 @@ def resource_update(context, data_dict): # Only allow resource update if the user and resource packages groups intersect userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups(), resource.package.get_groups() ): + if not _groups_intersect( userobj.get_groups('publisher'), resource.resource_group.package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to edit resources in this package') % str(user)} # check authentication against package @@ -65,14 +67,16 @@ def package_change_state(context, data_dict): user = context['user'] package = get_package_object(context, data_dict) - userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups(), package.get_groups() ): - return {'success': False, 'msg': _('User %s not authorized to change this package state') % str(user)} - authorized = check_access_old(package, model.Action.CHANGE_STATE, context) if not authorized: return {'success': False, 'msg': _('User %s not authorized to change state of package %s') % (str(user),package.id)} else: + userobj = model.User.get( user ) + if not userobj or \ + not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): + return {'success': False, + 'msg': _('User %s not authorized to change this package state') % str(user)} + return {'success': True} def package_edit_permissions(context, data_dict): @@ -82,7 +86,7 @@ def package_edit_permissions(context, data_dict): # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups(), package.get_groups() ): + if not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to edit permissions of this package') % str(user)} authorized = check_access_old(package, model.Action.EDIT_PERMISSIONS, context) @@ -98,7 +102,7 @@ def group_update(context, data_dict): # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups(), group.get_groups() ): + if not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to edit this group') % str(user)} authorized = check_access_old(group, model.Action.EDIT, context) @@ -113,7 +117,7 @@ def group_change_state(context, data_dict): group = get_group_object(context, data_dict) userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups(), group.get_groups() ): + if not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to change state of group') % str(user)} authorized = check_access_old(group, model.Action.CHANGE_STATE, context) @@ -129,7 +133,7 @@ def group_edit_permissions(context, data_dict): # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups(), group.get_groups() ): + if not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to edit permissions of group') % str(user)} authorized = check_access_old(group, model.Action.EDIT_PERMISSIONS, context) @@ -170,9 +174,11 @@ def user_update(context, data_dict): return {'success': False, 'msg': _('User %s not authorized to edit user %s') % (str(user), user_obj.id)} # Only allow package update if the user and package groups intersect or user is editing self - if (user != user_obj.name) and \ - not _groups_intersect( current_user.get_groups(), user_obj.get_groups() ): - return {'success': False, 'msg': _('User %s not authorized to edit user') % str(user)} + + if (user != user_obj.name): + current_user = model.User.get( user ) + if not _groups_intersect( current_user.get_groups('publisher'), user_obj.get_groups('publisher') ): + return {'success': False, 'msg': _('User %s not authorized to edit user') % str(user)} return {'success': True} diff --git a/ckan/model/group.py b/ckan/model/group.py index 8c20c23ae16..f798594f091 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -136,7 +136,7 @@ def add_package_by_name(self, package_name): member = Member(group=self, table_id=package.id, table_name='package') Session.add(member) - def get_groups(self): + def get_groups(self, group_type=None): """ Get all groups that this group is within """ import ckan.model as model if '_groups' not in self.__dict__: @@ -145,7 +145,11 @@ def get_groups(self): model.Member.table_name == 'group').\ filter(model.Member.state == 'active').\ filter(model.Member.table_id == self.id).all() - return self._groups + + if not group_type: + return self._groups + return [ x for x in self._groups if x.type == group_type ] + @property diff --git a/ckan/model/package.py b/ckan/model/package.py index c39ea0d6990..84f1ddb9bf7 100644 --- a/ckan/model/package.py +++ b/ckan/model/package.py @@ -501,7 +501,7 @@ def metadata_modified(self): def is_in_group(self, group): return group in self.get_groups() - def get_groups(self): + def get_groups(self, group_type=None): import ckan.model as model if '_groups' not in self.__dict__: self._groups = model.Session.query(model.Group).\ @@ -510,7 +510,10 @@ def get_groups(self): join(model.Package, model.Package.id == model.Member.table_id).\ filter(model.Member.state == 'active').\ filter(model.Member.table_id == self.id).all() - return self._groups + + if not group_type: + return self._groups + return [ x for x in self._groups if x.type == group_type ] @property def metadata_created(self): diff --git a/ckan/model/user.py b/ckan/model/user.py index 04c9e45d8eb..3bb7480deda 100644 --- a/ckan/model/user.py +++ b/ckan/model/user.py @@ -148,7 +148,7 @@ def number_administered_packages(self): def is_in_group(self, group): return group in self.get_groups() - def get_groups(self): + def get_groups(self, group_type=None): import ckan.model as model if '_groups' not in self.__dict__: self._groups = model.Session.query(model.Group).\ @@ -157,7 +157,11 @@ def get_groups(self): join(model.User, model.User.id == model.Member.table_id).\ filter(model.Member.state == 'active').\ filter(model.Member.table_id == self.id).all() - return self._groups + + if not group_type: + return self._groups + return [ x for x in self._groups if x.type == group_type ] + @classmethod def search(cls, querystr, sqlalchemy_query=None): From eae37ad112bb5f66577669162bffa357ef40106b Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 24 Jan 2012 15:27:05 +0000 Subject: [PATCH 13/59] [1669] Rebuild auth functions for publisher profile removing ALL signs of the old auth from the code, and some changes to make less tests fail --- ckan/lib/create_test_data.py | 10 ++ ckan/logic/auth/publisher/create.py | 98 +++++------------ ckan/logic/auth/publisher/delete.py | 38 ++++--- ckan/logic/auth/publisher/get.py | 72 ++++--------- ckan/logic/auth/publisher/update.py | 130 +++-------------------- ckan/model/group.py | 11 +- ckan/model/package.py | 11 +- ckan/model/user.py | 13 ++- ckan/tests/functional/api/__init__.py | 1 + ckan/tests/functional/api/test_action.py | 9 +- 10 files changed, 119 insertions(+), 274 deletions(-) diff --git a/ckan/lib/create_test_data.py b/ckan/lib/create_test_data.py index 1a45b9ad86c..7804f96bd1f 100644 --- a/ckan/lib/create_test_data.py +++ b/ckan/lib/create_test_data.py @@ -318,8 +318,16 @@ def create(cls): * Package: warandpeace * Associated tags, etc etc ''' + + publisher_group = model.Group(name=u'default_publisher', + title=u'Default publisher', + description=u'The default test publisher', + type='publisher') + model.Session.add(publisher_group) + cls.pkg_names = [u'annakarenina', u'warandpeace'] pkg1 = model.Package(name=cls.pkg_names[0]) + pkg1.group = publisher_group model.Session.add(pkg1) pkg1.title = u'A Novel By Tolstoy' pkg1.version = u'0.7a' @@ -373,6 +381,8 @@ def create(cls): tag1 = model.Tag(name=u'russian') tag2 = model.Tag(name=u'tolstoy') + pkg2.group = publisher_group + # Flexible tag, allows spaces, upper-case, # and all punctuation except commas tag3 = model.Tag(name=u'Flexible \u30a1') diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index cfeaa6a5f5e..cddb9f77e18 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -1,5 +1,3 @@ -# Updated: False - from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ get_user_object, get_resource_object from ckan.logic.auth.publisher import _groups_intersect @@ -11,16 +9,17 @@ def package_create(context, data_dict=None): model = context['model'] user = context['user'] - - check1 = check_access_old(model.System(), model.Action.PACKAGE_CREATE, context) - if not check1: - return {'success': False, 'msg': _('User %s not authorized to create packages') % str(user)} - else: - - check2 = check_group_auth(context,data_dict) - if not check2: - return {'success': False, 'msg': _('User %s not authorized to edit these groups') % str(user)} + # We need the publisher group passed in as part of this request + try: + group = get_group_object( context ) + except NotFound: + return {'success': False, + 'msg': _('User %s not authorized to create a package without a group specified') % str(user)} + + userobj = model.User.get( user ) + if not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): + return {'success': False, 'msg': _('User %s not authorized to create a package here') % str(user)} return {'success': True} @@ -33,13 +32,11 @@ def package_relationship_create(context, data_dict): id = data_dict['id'] id2 = data_dict['id2'] - pkg1 = model.Package.get(id) - pkg2 = model.Package.get(id2) + pkg1grps = model.Package.get(id).get_groups('publisher') + pkg2grps = model.Package.get(id2).get_groups('publisher') - authorized = Authorizer().\ - authorized_package_relationship(\ - user, pkg1, pkg2, action=model.Action.EDIT) - + usergrps = model.User.get( user ).get_groups('publisher') + authorized = _groups_intersect( usergrps, pkg1grps ) and _groups_intersect( usergrps, pkg2grps ) if not authorized: return {'success': False, 'msg': _('User %s not authorized to edit these packages') % str(user)} else: @@ -50,75 +47,32 @@ def group_create(context, data_dict=None): user = context['user'] # TODO: We need to check whether this group is being created within another group - - authorized = check_access_old(model.System(), model.Action.GROUP_CREATE, context) + try: + group = get_group_object( context ) + except NotFound: + return { 'success' : True } + + usergrps = User.get( user ).get_groups('publisher') + authorized = _groups_intersect( usergrps, group.get_groups('publisher') ) if not authorized: return {'success': False, 'msg': _('User %s not authorized to create groups') % str(user)} else: return {'success': True} def authorization_group_create(context, data_dict=None): - model = context['model'] - user = context['user'] - - # TODO: We need to check whether this group is being created within another group - - authorized = check_access_old(model.System(), model.Action.AUTHZ_GROUP_CREATE, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to create authorization groups') % str(user)} - else: - return {'success': True} + return {'success': False, 'msg': _('Authorization groups not implemented in this profile') % str(user)} + def rating_create(context, data_dict): # No authz check in the logic function return {'success': True} def user_create(context, data_dict=None): - model = context['model'] - user = context['user'] - - authorized = check_access_old(model.System(), model.Action.USER_CREATE, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to create users') % str(user)} - else: - return {'success': True} - -def check_group_auth(context, data_dict): - if not data_dict: - return True + return {'success': True} - model = context['model'] - pkg = context.get("package") - - api_version = context.get('api_version') or '1' - - group_blobs = data_dict.get("groups", []) - groups = set() - for group_blob in group_blobs: - # group_blob might be a dict or a group_ref - if isinstance(group_blob, dict): - if api_version == '1': - id = group_blob.get('name') - else: - id = group_blob.get('id') - if not id: - continue - else: - id = group_blob - grp = model.Group.get(id) - if grp is None: - raise NotFound(_('Group was not found.')) - groups.add(grp) - - if pkg: - pkg_groups = pkg.get_groups() - - groups = groups - set(pkg_groups) - - for group in groups: - if not check_access_old(group, model.Action.EDIT, context): - return False +def check_group_auth(context, data_dict): + # Maintained for function count in profiles, until we can rename to _* return True ## Modifications for rest api diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index cc9de97b9ba..d0196d6d96f 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -1,9 +1,8 @@ -# Updated: False from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ get_user_object, get_resource_object -from ckan.logic import check_access_old from ckan.logic.auth import get_package_object, get_group_object -from ckan.logic.auth.create import package_relationship_create +from ckan.logic.auth.publisher import _groups_intersect +from ckan.logic.auth.publisher.create import package_relationship_create from ckan.authz import Authorizer from ckan.lib.base import _ @@ -11,13 +10,14 @@ def package_delete(context, data_dict): model = context['model'] user = context['user'] package = get_package_object(context, data_dict) + userobj = model.User.get( user ) - authorized = check_access_old(package, model.Action.PURGE, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to delete package %s') % (str(user),package.id)} - else: - return {'success': True} - + if not userobj or \ + not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): + return {'success': False, + 'msg': _('User %s not authorized to delete packages in these group') % str(user)} + return {'success': True} + def package_relationship_delete(context, data_dict): return package_relationship_create(context, data_dict) @@ -26,18 +26,24 @@ def relationship_delete(context, data_dict): user = context['user'] relationship = context['relationship'] - authorized = check_access_old(relationship, model.Action.PURGE, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to delete relationship %s') % (str(user),relationship.id)} - else: - return {'success': True} + pkg1groups = set( relationship.package1.get_groups('publisher') ) + pkg2groups = set (relationship.package2.get_groups('publisher') ) + usergrps = model.User.get( user ).get_groups('publisher') + + if _groups_intersect( usergrps, pkg1groups ) and _groups_intersect( usergrps, pkg2groups ): + return {'success': True} + + return {'success': False, 'msg': _('User %s not authorized to delete relationship %s') % (str(user),relationship.id)} + def group_delete(context, data_dict): model = context['model'] user = context['user'] + group = get_group_object(context, data_dict) - - authorized = check_access_old(group, model.Action.PURGE, context) + usergrps = model.User.get( user ).get_groups('publisher', 'admin') + + authorized = _groups_intersect( usergrps, group.get_groups('publisher') ) if not authorized: return {'success': False, 'msg': _('User %s not authorized to delete group %s') % (str(user),group.id)} else: diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index 95f8b188254..e433be441b4 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -1,9 +1,7 @@ -# Updated: False from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ get_user_object, get_resource_object -from ckan.logic import check_access_old, NotFound -from ckan.authz import Authorizer from ckan.lib.base import _ +from ckan.logic.auth.publisher import _groups_intersect from ckan.logic.auth import get_package_object, get_group_object, get_resource_object @@ -14,12 +12,6 @@ def site_read(context, data_dict): ./ckan/controllers/api.py """ - model = context['model'] - user = context.get('user') - - if not Authorizer().is_authorized(user, model.Action.SITE_READ, model.System): - return {'success': False, 'msg': _('Not authorized to see this page')} - return {'success': True} def package_search(context, data_dict): @@ -66,55 +58,29 @@ def user_list(context, data_dict): return {'success': True} def package_relationships_list(context, data_dict): - model = context['model'] - user = context.get('user') - - id = data_dict['id'] - id2 = data_dict.get('id2') - pkg1 = model.Package.get(id) - pkg2 = model.Package.get(id2) - - authorized = Authorizer().\ - authorized_package_relationship(\ - user, pkg1, pkg2, action=model.Action.READ) - - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to read these packages') % str(user)} - else: - return {'success': True} + return {'success': True} def package_show(context, data_dict): model = context['model'] user = context.get('user') package = get_package_object(context, data_dict) - - authorized = check_access_old(package, model.Action.READ, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} - else: - return {'success': True} + userobj = model.User.get( user ) + + if package.state == 'deleted': + if not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): + return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} + + return {'success': True} def resource_show(context, data_dict): model = context['model'] user = context.get('user') resource = get_resource_object(context, data_dict) - - # check authentication against package - query = model.Session.query(model.Package)\ - .join(model.ResourceGroup)\ - .join(model.Resource)\ - .filter(model.ResourceGroup.id == resource.resource_group_id) - pkg = query.first() - if not pkg: - raise NotFound(_('No package found for this resource, cannot check auth.')) + package = resource.revision_group.package pkg_dict = {'id': pkg.id} - authorized = package_show(context, pkg_dict).get('success') - - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (str(user), resource.id)} - else: - return {'success': True} + return package_show(context, pkg_dict) + def revision_show(context, data_dict): # No authz check in the logic function @@ -124,12 +90,14 @@ def group_show(context, data_dict): model = context['model'] user = context.get('user') group = get_group_object(context, data_dict) - - authorized = check_access_old(group, model.Action.READ, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to read group %s') % (str(user),group.id)} - else: - return {'success': True} + userobj = model.User.get( user ) + + if group.state == 'deleted': + if not user or \ + not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): + return {'success': False, 'msg': _('User %s not authorized to show group %s') % (str(user),group.id)} + + return {'success': True} def tag_show(context, data_dict): # No authz check in the logic function diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 55b108828d3..ed77302c558 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -1,8 +1,7 @@ -from ckan.logic import check_access_old, NotFound from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ get_user_object, get_resource_object from ckan.logic.auth.publisher import _groups_intersect -from ckan.logic.auth.create import check_group_auth, package_relationship_create +from ckan.logic.auth.publisher.create import package_relationship_create from ckan.authz import Authorizer from ckan.lib.base import _ @@ -14,86 +13,34 @@ def package_update(context, data_dict): user = context.get('user') package = get_package_object(context, data_dict) - # Only allow package update if the user and package groups intersect - check1 = check_access_old(package, model.Action.EDIT, context) - if not check1: - return {'success': False, 'msg': _('User %s not authorized to edit package %s') % (str(user), package.id)} - else: - check2 = check_group_auth(context,data_dict) - if not check2: - return {'success': False, 'msg': _('User %s not authorized to edit these groups') % str(user)} - userobj = model.User.get( user ) if not userobj or \ not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to edit packages in these groups') % str(user)} - return {'success': True} def resource_update(context, data_dict): model = context['model'] user = context.get('user') resource = get_resource_object(context, data_dict) - - # Only allow resource update if the user and resource packages groups intersect userobj = model.User.get( user ) + if not _groups_intersect( userobj.get_groups('publisher'), resource.resource_group.package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to edit resources in this package') % str(user)} - # check authentication against package - query = model.Session.query(model.Package)\ - .join(model.ResourceGroup)\ - .join(model.Resource)\ - .filter(model.ResourceGroup.id == resource.resource_group_id) - pkg = query.first() - if not pkg: - raise NotFound(_('No package found for this resource, cannot check auth.')) - - pkg_dict = {'id': pkg.id} - authorized = package_update(context, pkg_dict).get('success') - - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to read edit %s') % (str(user), resource.id)} - else: - return {'success': True} + return {'success': True} def package_relationship_update(context, data_dict): return package_relationship_create(context, data_dict) def package_change_state(context, data_dict): - model = context['model'] - user = context['user'] - package = get_package_object(context, data_dict) - - authorized = check_access_old(package, model.Action.CHANGE_STATE, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to change state of package %s') % (str(user),package.id)} - else: - userobj = model.User.get( user ) - if not userobj or \ - not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): - return {'success': False, - 'msg': _('User %s not authorized to change this package state') % str(user)} - - return {'success': True} + return package_update( context, data_dict ) def package_edit_permissions(context, data_dict): - model = context['model'] - user = context['user'] - package = get_package_object(context, data_dict) - - # Only allow package update if the user and package groups intersect - userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): - return {'success': False, 'msg': _('User %s not authorized to edit permissions of this package') % str(user)} - - authorized = check_access_old(package, model.Action.EDIT_PERMISSIONS, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to edit permissions of package %s') % (str(user),package.id)} - else: - return {'success': True} + return {'success': False, + 'msg': _('Package edit permissions is not available')} def group_update(context, data_dict): model = context['model'] @@ -102,67 +49,23 @@ def group_update(context, data_dict): # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): + if not _groups_intersect( userobj.get_groups('publisher', 'admin'), group.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to edit this group') % str(user)} - authorized = check_access_old(group, model.Action.EDIT, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to edit group %s') % (str(user),group.id)} - else: - return {'success': True} + return {'success': True} def group_change_state(context, data_dict): - model = context['model'] - user = context['user'] - group = get_group_object(context, data_dict) - - userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): - return {'success': False, 'msg': _('User %s not authorized to change state of group') % str(user)} - - authorized = check_access_old(group, model.Action.CHANGE_STATE, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to change state of group %s') % (str(user),group.id)} - else: - return {'success': True} + return group_update(context, data_dict) def group_edit_permissions(context, data_dict): - model = context['model'] - user = context['user'] - group = get_group_object(context, data_dict) - - # Only allow package update if the user and package groups intersect - userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): - return {'success': False, 'msg': _('User %s not authorized to edit permissions of group') % str(user)} - - authorized = check_access_old(group, model.Action.EDIT_PERMISSIONS, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to edit permissions of group %s') % (str(user),group.id)} - else: - return {'success': True} + return {'success': False, 'msg': _('Group edit permissions is not implemented')} def authorization_group_update(context, data_dict): - model = context['model'] - user = context['user'] - authorization_group = get_authorization_group_object(context, data_dict) + return {'success': False, 'msg': _('Authorization group update not implemented')} - authorized = check_access_old(authorization_group, model.Action.EDIT, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to edit permissions of authorization group %s') % (str(user),authorization_group.id)} - else: - return {'success': True} def authorization_group_edit_permissions(context, data_dict): - model = context['model'] - user = context['user'] - authorization_group = get_authorization_group_object(context, data_dict) - - authorized = check_access_old(authorization_group, model.Action.EDIT_PERMISSIONS, context) - if not authorized: - return {'success': False, 'msg': _('User %s not authorized to edit permissions of authorization group %s') % (str(user),authorization_group.id)} - else: - return {'success': True} + return {'success': False, 'msg': _('Authorization group update not implemented')} def user_update(context, data_dict): model = context['model'] @@ -173,20 +76,13 @@ def user_update(context, data_dict): not ('reset_key' in data_dict and data_dict['reset_key'] == user_obj.reset_key): return {'success': False, 'msg': _('User %s not authorized to edit user %s') % (str(user), user_obj.id)} - # Only allow package update if the user and package groups intersect or user is editing self - - if (user != user_obj.name): - current_user = model.User.get( user ) - if not _groups_intersect( current_user.get_groups('publisher'), user_obj.get_groups('publisher') ): - return {'success': False, 'msg': _('User %s not authorized to edit user') % str(user)} - return {'success': True} def revision_change_state(context, data_dict): model = context['model'] user = context['user'] - authorized = Authorizer().is_authorized(user, model.Action.CHANGE_STATE, model.Revision) + authorized = Authorizer().is_sysadmin(unicode(user)) if not authorized: return {'success': False, 'msg': _('User %s not authorized to change state of revision' ) % str(user)} else: diff --git a/ckan/model/group.py b/ckan/model/group.py index f798594f091..b5f2d4f43ec 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -136,7 +136,7 @@ def add_package_by_name(self, package_name): member = Member(group=self, table_id=package.id, table_name='package') Session.add(member) - def get_groups(self, group_type=None): + def get_groups(self, group_type=None, capacity=None): """ Get all groups that this group is within """ import ckan.model as model if '_groups' not in self.__dict__: @@ -146,9 +146,12 @@ def get_groups(self, group_type=None): filter(model.Member.state == 'active').\ filter(model.Member.table_id == self.id).all() - if not group_type: - return self._groups - return [ x for x in self._groups if x.type == group_type ] + groups = self._groups + if group_type: + groups = [g for g in groups if g.type == group_type] + if capacity: + groups = [g for g in groups if g.capacity == capacity] + return groups diff --git a/ckan/model/package.py b/ckan/model/package.py index 84f1ddb9bf7..70df1c6d448 100644 --- a/ckan/model/package.py +++ b/ckan/model/package.py @@ -501,7 +501,7 @@ def metadata_modified(self): def is_in_group(self, group): return group in self.get_groups() - def get_groups(self, group_type=None): + def get_groups(self, group_type=None, capacity=None): import ckan.model as model if '_groups' not in self.__dict__: self._groups = model.Session.query(model.Group).\ @@ -511,9 +511,12 @@ def get_groups(self, group_type=None): filter(model.Member.state == 'active').\ filter(model.Member.table_id == self.id).all() - if not group_type: - return self._groups - return [ x for x in self._groups if x.type == group_type ] + groups = self._groups + if group_type: + groups = [g for g in groups if g.type == group_type] + if capacity: + groups = [g for g in groups if g.capacity == capacity] + return groups @property def metadata_created(self): diff --git a/ckan/model/user.py b/ckan/model/user.py index 3bb7480deda..51c8cf1961a 100644 --- a/ckan/model/user.py +++ b/ckan/model/user.py @@ -148,7 +148,7 @@ def number_administered_packages(self): def is_in_group(self, group): return group in self.get_groups() - def get_groups(self, group_type=None): + def get_groups(self, group_type=None, capacity=None): import ckan.model as model if '_groups' not in self.__dict__: self._groups = model.Session.query(model.Group).\ @@ -157,10 +157,13 @@ def get_groups(self, group_type=None): join(model.User, model.User.id == model.Member.table_id).\ filter(model.Member.state == 'active').\ filter(model.Member.table_id == self.id).all() - - if not group_type: - return self._groups - return [ x for x in self._groups if x.type == group_type ] + + groups = self._groups + if group_type: + groups = [g for g in groups if g.type == group_type] + if capacity: + groups = [g for g in groups if g.capacity == capacity] + return groups @classmethod diff --git a/ckan/tests/functional/api/__init__.py b/ckan/tests/functional/api/__init__.py index f5667c286de..208b40157d3 100644 --- a/ckan/tests/functional/api/__init__.py +++ b/ckan/tests/functional/api/__init__.py @@ -31,4 +31,5 @@ def assert_dicts_equal_ignoring_ordering(dict1, dict2): dicts = [copy.deepcopy(dict1), copy.deepcopy(dict2)] for d in dicts: d = change_lists_to_sets(d) + from nose.tools import set_trace; set_trace() assert_equal(dicts[0], dicts[1]) diff --git a/ckan/tests/functional/api/test_action.py b/ckan/tests/functional/api/test_action.py index a80d52948a3..5b6eb327058 100644 --- a/ckan/tests/functional/api/test_action.py +++ b/ckan/tests/functional/api/test_action.py @@ -252,7 +252,7 @@ def test_05_user_show_edits(self): assert 'timestamp' in edit assert_equal(edit['state'], 'active') assert_equal(edit['approved_timestamp'], None) - assert_equal(set(edit['groups']), set(('roger', 'david'))) + assert_equal(set(edit['groups']), set(('default_publisher', 'roger', 'david'))) assert_equal(edit['state'], 'active') assert edit['message'].startswith('Creating test data.') assert_equal(set(edit['packages']), set(('warandpeace', 'annakarenina'))) @@ -546,7 +546,8 @@ def test_13_group_list(self): { 'result': [ 'david', - 'roger' + 'roger', + 'default_publisher' ], 'help': 'Returns a list of groups', 'success': True @@ -561,8 +562,8 @@ def test_13_group_list(self): assert res_obj['result'][0]['name'] == 'david' assert res_obj['result'][0]['display_name'] == 'Dave\'s books' assert res_obj['result'][0]['packages'] == 2 - assert res_obj['result'][1]['name'] == 'roger' - assert res_obj['result'][1]['packages'] == 1 + assert res_obj['result'][2]['name'] == 'roger', res_obj['result'][1] + assert res_obj['result'][2]['packages'] == 1 assert 'id' in res_obj['result'][0] assert 'revision_id' in res_obj['result'][0] assert 'state' in res_obj['result'][0] From aed066895568e3ad7d7e8c90ac9ca5bd9cbfa6d3 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Thu, 26 Jan 2012 09:25:04 +0000 Subject: [PATCH 14/59] [1669, xl] Publisher profile changes, see full commit message. * Added the chosen library from http://harvesthq.github.com/chosen/ to provide useful selects and multi-selects. Was/will be used for choosing the parent groups of the publisher. * Added internal plugin/extension for the basic publisher form. Uses IGroupForm interface to provide a publisher group which a form for creating/editing publishers which supports: 1. Adding users with a capacity 2. Adding datasets * Updated setup.py to include the publisher form plugin * Removed IGroupForm route checking (check existence of url before setting) as it caused problems with this version of Routes. * Fixed redirect to /{{type}}/name instead of /group/name * Updated users key in group schema to allow it to specify capacity * Fixed the group model filtering within get_groups to not cache if capacity is used as a filter * Implemented an autocomplete call in application.js for users within a publisher form (behaves differently from the current user autocomplete) * Fixed publisher_form template to ensure object__num__field naming was consistent for users. * Temporarily removed publisher parent for the simple case. --- ckan/config/routing.py | 2 +- ckan/controllers/group.py | 17 +- ckan/lib/dictization/model_save.py | 3 +- ckan/logic/action/create.py | 1 + ckan/logic/schema.py | 5 + ckan/model/group.py | 7 +- ckan/model/user.py | 18 +- ckan/public/css/chosen.css | 390 ++++++++ ckan/public/css/style.css | 3 +- ckan/public/images/chosen-sprite.png | Bin 0 -> 1560 bytes ckan/public/scripts/application.js | 43 + .../vendor/jquery.chosen/0.9.7/chosen.js | 902 ++++++++++++++++++ ckan/templates/layout_base.html | 4 + ckanext/publisher_form/__init__.py | 6 + ckanext/publisher_form/forms.py | 112 +++ .../templates/publisher_form.html | 135 +++ setup.py | 1 + 17 files changed, 1625 insertions(+), 24 deletions(-) create mode 100644 ckan/public/css/chosen.css create mode 100644 ckan/public/images/chosen-sprite.png create mode 100644 ckan/public/scripts/vendor/jquery.chosen/0.9.7/chosen.js create mode 100644 ckanext/publisher_form/__init__.py create mode 100644 ckanext/publisher_form/forms.py create mode 100644 ckanext/publisher_form/templates/publisher_form.html diff --git a/ckan/config/routing.py b/ckan/config/routing.py index b9f64c08efc..cae76e8d7cf 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -244,9 +244,9 @@ def make_map(): ) map.connect('group_read', '/group/{id}', controller='group', action='read') - register_package_behaviour(map) register_group_behaviour(map) + # authz group map.redirect("/authorizationgroups", "/authorizationgroup") diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index c5f734aad6a..02e29b7fc34 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -50,13 +50,9 @@ def register_pluggable_behaviour(map): # Our version of routes doesn't allow the environ to be passed into the match call # and so we have to set it on the map instead. This looks like a threading problem # waiting to happen but it is executed sequentially from instead the routing setup - e = map.environ - map.environ = {'REQUEST_METHOD': 'GET'} - match = map.match('/%s/new' % (group_type,)) - map.environ = e - if match: - raise Exception, "Plugin %r would overwrite existing urls" % plugin - + + map.connect('%s_index' % (group_type,), + '/%s' % (group_type,), controller='group', action='index') map.connect('%s_new' % (group_type,), '/%s/new' % (group_type,), controller='group', action='new') map.connect('%s_read' % (group_type,), @@ -303,7 +299,6 @@ def edit(self, id, data=None, errors=None, error_summary=None): group = context.get("group") c.group = group - try: check_access('group_update',context) except NotAuthorized, e: @@ -363,17 +358,17 @@ def _save_new(self, context, group_type=None): return self.new(data_dict, errors, error_summary) def _save_edit(self, id, context): - try: + try: data_dict = clean_dict(unflatten( tuplize_dict(parse_params(request.params)))) context['message'] = data_dict.get('log_message', '') data_dict['id'] = id group = get_action('group_update')(context, data_dict) - h.redirect_to(controller='group', action='read', id=group['name']) + h.redirect_to('%s_read' % group['type'], id=group['name']) except NotAuthorized: abort(401, _('Unauthorized to read group %s') % id) except NotFound, e: - abort(404, _('Package not found')) + abort(404, _('Group not found')) except DataError: abort(400, _(u'Integrity Error')) except ValidationError, e: diff --git a/ckan/lib/dictization/model_save.py b/ckan/lib/dictization/model_save.py index aaa4569fc12..3dcb5bb00c3 100644 --- a/ckan/lib/dictization/model_save.py +++ b/ckan/lib/dictization/model_save.py @@ -285,7 +285,6 @@ def package_dict_save(pkg_dict, context): return pkg def group_member_save(context, group_dict, member_table_name): - model = context["model"] session = context["session"] group = context['group'] @@ -336,7 +335,7 @@ def group_dict_save(group_dict, context): group = table_dict_save(group_dict, Group, context) context['group'] = group - + print group_dict group_member_save(context, group_dict, 'packages') group_member_save(context, group_dict, 'users') group_member_save(context, group_dict, 'groups') diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index 48dc13d4567..52e8d3cf36a 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -162,6 +162,7 @@ def group_create(context, data_dict): group = group_dict_save(data, context) + # TODO: Refactor to use new member for admin if user: admins = [model.User.by_name(user.decode('utf8'))] else: diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 3142df75a95..e1a262d0fd0 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -179,6 +179,11 @@ def group_form_schema(): "name": [not_empty, unicode], "__extras": [ignore] } + schema['users'] = { + "name": [not_empty, unicode], + "capacity": [ignore_missing], + "__extras": [ignore] + } return schema diff --git a/ckan/model/group.py b/ckan/model/group.py index b5f2d4f43ec..47c82ffb677 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -7,7 +7,7 @@ from package import * from types import make_uuid import vdm.sqlalchemy -from ckan.model import extension +from ckan.model import extension, User from sqlalchemy.ext.associationproxy import association_proxy __all__ = ['group_table', 'Group', 'package_revision_table', @@ -123,11 +123,14 @@ def search_by_name(cls, text_query): def as_dict(self, ref_package_by='name'): _dict = DomainObject.as_dict(self) - _dict['packages'] = [getattr(package, ref_package_by) for package in self.packages] + _dict['packages'] = [getattr(package, ref_package_by) for package in self.packages] _dict['extras'] = dict([(key, value) for key, value in self.extras.items()]) + if ( self.type == 'publisher' ): + _dict['users'] = [getattr(user, "name") for user in self.members_of_type(User)] return _dict def add_package_by_name(self, package_name): + from pdb import set_trace; set_trace() if not package_name: return package = Package.by_name(package_name) diff --git a/ckan/model/user.py b/ckan/model/user.py index 51c8cf1961a..ed07c0c5e37 100644 --- a/ckan/model/user.py +++ b/ckan/model/user.py @@ -150,19 +150,23 @@ def is_in_group(self, group): def get_groups(self, group_type=None, capacity=None): import ckan.model as model - if '_groups' not in self.__dict__: - self._groups = model.Session.query(model.Group).\ - join(model.Member, model.Member.group_id == model.Group.id and\ - model.Member.table_name == 'user' ).\ + + q = model.Session.query(model.Group)\ + .join(model.Member, model.Member.group_id == model.Group.id and \ + model.Member.table_name == 'user' ).\ join(model.User, model.User.id == model.Member.table_id).\ filter(model.Member.state == 'active').\ - filter(model.Member.table_id == self.id).all() + filter(model.Member.table_id == self.id) + if capacity: + q = q.filter( model.Member.capacity == capacity ) + return q.all() + + if '_groups' not in self.__dict__: + self._groups = q.all() groups = self._groups if group_type: groups = [g for g in groups if g.type == group_type] - if capacity: - groups = [g for g in groups if g.capacity == capacity] return groups diff --git a/ckan/public/css/chosen.css b/ckan/public/css/chosen.css new file mode 100644 index 00000000000..1405ebbfc5a --- /dev/null +++ b/ckan/public/css/chosen.css @@ -0,0 +1,390 @@ +/* @group Base */ +.chzn-container { + font-size: 13px; + position: relative; + display: inline-block; + zoom: 1; + *display: inline; +} +.chzn-container .chzn-drop { + background: #fff; + border: 1px solid #aaa; + border-top: 0; + position: absolute; + top: 29px; + left: 0; + -webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15); + -moz-box-shadow : 0 4px 5px rgba(0,0,0,.15); + -o-box-shadow : 0 4px 5px rgba(0,0,0,.15); + box-shadow : 0 4px 5px rgba(0,0,0,.15); + z-index: 999; +} +/* @end */ + +/* @group Single Chosen */ +.chzn-container-single .chzn-single { + background-color: #ffffff; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #f4f4f4), color-stop(0.48, #eeeeee), color-stop(0.5, #f6f6f6), color-stop(0.8, #ffffff)); + background-image: -webkit-linear-gradient(center bottom, #f4f4f4 0%, #eeeeee 48%, #f6f6f6 50%, #ffffff 80%); + background-image: -moz-linear-gradient(center bottom, #f4f4f4 0%, #eeeeee 48%, #f6f6f6 50%, #ffffff 80%); + background-image: -o-linear-gradient(top, #f4f4f4 0%, #eeeeee 48%, #f6f6f6 50%, #ffffff 80%); + background-image: -ms-linear-gradient(top, #f4f4f4 0%, #eeeeee 48%, #f6f6f6 50%, #ffffff 80%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 ); + background-image: linear-gradient(top, #f4f4f4 0%, #eeeeee 48%, #f6f6f6 50%, #ffffff 80%); + -webkit-border-radius: 5px; + -moz-border-radius : 5px; + border-radius : 5px; + -moz-background-clip : padding; + -webkit-background-clip: padding-box; + background-clip : padding-box; + border: 1px solid #aaaaaa; + -webkit-box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); + -moz-box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); + box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); + display: block; + overflow: hidden; + white-space: nowrap; + position: relative; + height: 23px; + line-height: 24px; + padding: 0 0 0 8px; + color: #444444; + text-decoration: none; +} +.chzn-container-single .chzn-single span { + margin-right: 26px; + display: block; + overflow: hidden; + white-space: nowrap; + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; + text-overflow: ellipsis; +} +.chzn-container-single .chzn-single abbr { + display: block; + position: absolute; + right: 26px; + top: 6px; + width: 12px; + height: 13px; + font-size: 1px; + background: url('../images/chosen-sprite.png') right top no-repeat; +} +.chzn-container-single .chzn-single abbr:hover { + background-position: right -11px; +} +.chzn-container-single .chzn-single div { + position: absolute; + right: 0; + top: 0; + display: block; + height: 100%; + width: 18px; +} +.chzn-container-single .chzn-single div b { + background: url('../images/chosen-sprite.png') no-repeat 0 0; + display: block; + width: 100%; + height: 100%; +} +.chzn-container-single .chzn-search { + padding: 3px 4px; + position: relative; + margin: 0; + white-space: nowrap; + z-index: 1010; +} +.chzn-container-single .chzn-search input { + background: #fff url('../images/chosen-sprite.png') no-repeat 100% -22px; + background: url('../images/chosen-sprite.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); + background: url('../images/chosen-sprite.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background: url('../images/chosen-sprite.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background: url('../images/chosen-sprite.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); + background: url('../images/chosen-sprite.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); + background: url('../images/chosen-sprite.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%,#eeeeee 99%); + margin: 1px 0; + padding: 4px 20px 4px 5px; + outline: 0; + border: 1px solid #aaa; + font-family: sans-serif; + font-size: 1em; +} +.chzn-container-single .chzn-drop { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius : 0 0 4px 4px; + border-radius : 0 0 4px 4px; + -moz-background-clip : padding; + -webkit-background-clip: padding-box; + background-clip : padding-box; +} +/* @end */ + +.chzn-container-single-nosearch .chzn-search input { + position: absolute; + left: -9000px; +} + +/* @group Multi Chosen */ +.chzn-container-multi .chzn-choices { + background-color: #fff; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); + background-image: -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background-image: -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background-image: -o-linear-gradient(bottom, white 85%, #eeeeee 99%); + background-image: -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); + background-image: linear-gradient(top, #ffffff 85%, #eeeeee 99%); + border: 1px solid #aaa; + margin: 0; + padding: 0; + cursor: text; + overflow: hidden; + height: auto !important; + height: 1%; + position: relative; +} +.chzn-container-multi .chzn-choices li { + float: left; + list-style: none; +} +.chzn-container-multi .chzn-choices .search-field { + white-space: nowrap; + margin: 0; + padding: 0; +} +.chzn-container-multi .chzn-choices .search-field input { + color: #666; + background: transparent !important; + border: 0 !important; + font-family: sans-serif; + font-size: 100%; + height: 15px; + padding: 5px; + margin: 1px 0; + outline: 0; + -webkit-box-shadow: none; + -moz-box-shadow : none; + -o-box-shadow : none; + box-shadow : none; +} +.chzn-container-multi .chzn-choices .search-field .default { + color: #999; +} +.chzn-container-multi .chzn-choices .search-choice { + -webkit-border-radius: 3px; + -moz-border-radius : 3px; + border-radius : 3px; + -moz-background-clip : padding; + -webkit-background-clip: padding-box; + background-clip : padding-box; + background-color: #e4e4e4; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.48, #e8e8e8), color-stop(0.5, #f0f0f0), color-stop(0.8, #f4f4f4)); + background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, #e8e8e8 48%, #f0f0f0 50%, #f4f4f4 80%); + background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, #e8e8e8 48%, #f0f0f0 50%, #f4f4f4 80%); + background-image: -o-linear-gradient(top, #eeeeee 0%, #e8e8e8 48%, #f0f0f0 50%, #f4f4f4 80%); + background-image: -ms-linear-gradient(top, #eeeeee 0%, #e8e8e8 48%, #f0f0f0 50%, #f4f4f4 80%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#f4f4f4',GradientType=0 ); + background-image: linear-gradient(top, #eeeeee 0%, #e8e8e8 48%, #f0f0f0 50%, #f4f4f4 80%); + -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); + -moz-box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); + box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); + color: #333; + border: 1px solid #aaaaaa; + line-height: 13px; + padding: 3px 20px 3px 5px; + margin: 3px 0 3px 5px; + position: relative; +} +.chzn-container-multi .chzn-choices .search-choice span { + cursor: default; +} +.chzn-container-multi .chzn-choices .search-choice-focus { + background: #d4d4d4; +} +.chzn-container-multi .chzn-choices .search-choice .search-choice-close { + display: block; + position: absolute; + right: 3px; + top: 4px; + width: 12px; + height: 13px; + font-size: 1px; + background: url('../images/chosen-sprite.png') right top no-repeat; +} +.chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover { + background-position: right -11px; +} +.chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close { + background-position: right -11px; +} +/* @end */ + +/* @group Results */ +.chzn-container .chzn-results { + margin: 0 4px 4px 0; + max-height: 240px; + padding: 0 0 0 4px; + position: relative; + overflow-x: hidden; + overflow-y: auto; +} +.chzn-container-multi .chzn-results { + margin: -1px 0 0; + padding: 0; +} +.chzn-container .chzn-results li { + display: none; + line-height: 15px; + padding: 5px 6px; + margin: 0; + list-style: none; +} +.chzn-container .chzn-results .active-result { + cursor: pointer; + display: list-item; +} +.chzn-container .chzn-results .highlighted { + background-color: #3875d7; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.1, #2a62bc), color-stop(0.8, #3875d7)); + background-image: -webkit-linear-gradient(center bottom, #2a62bc 10%, #3875d7 80%); + background-image: -moz-linear-gradient(center bottom, #2a62bc 10%, #3875d7 80%); + background-image: -o-linear-gradient(bottom, #2a62bc 10%, #3875d7 80%); + background-image: -ms-linear-gradient(top, #2a62bc 10%, #3875d7 80%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2a62bc', endColorstr='#3875d7',GradientType=0 ); + background-image: linear-gradient(top, #2a62bc 10%, #3875d7 80%); + color: #fff; +} +.chzn-container .chzn-results li em { + background: #feffde; + font-style: normal; +} +.chzn-container .chzn-results .highlighted em { + background: transparent; +} +.chzn-container .chzn-results .no-results { + background: #f4f4f4; + display: list-item; +} +.chzn-container .chzn-results .group-result { + cursor: default; + color: #999; + font-weight: bold; +} +.chzn-container .chzn-results .group-option { + padding-left: 15px; +} +.chzn-container-multi .chzn-drop .result-selected { + display: none; +} +.chzn-container .chzn-results-scroll { + background: white; + margin: 0px 4px; + position: absolute; + text-align: center; + width: 321px; /* This should by dynamic with js */ + z-index: 1; +} +.chzn-container .chzn-results-scroll span { + display: inline-block; + height: 17px; + text-indent: -5000px; + width: 9px; +} +.chzn-container .chzn-results-scroll-down { + bottom: 0; +} +.chzn-container .chzn-results-scroll-down span { + background: url('../images/chosen-sprite.png') no-repeat -4px -3px; +} +.chzn-container .chzn-results-scroll-up span { + background: url('../images/chosen-sprite.png') no-repeat -22px -3px; +} +/* @end */ + +/* @group Active */ +.chzn-container-active .chzn-single { + -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); + -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); + -o-box-shadow : 0 0 5px rgba(0,0,0,.3); + box-shadow : 0 0 5px rgba(0,0,0,.3); + border: 1px solid #5897fb; +} +.chzn-container-active .chzn-single-with-drop { + border: 1px solid #aaa; + -webkit-box-shadow: 0 1px 0 #fff inset; + -moz-box-shadow : 0 1px 0 #fff inset; + -o-box-shadow : 0 1px 0 #fff inset; + box-shadow : 0 1px 0 #fff inset; + background-color: #eee; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.2, white), color-stop(0.8, #eeeeee)); + background-image: -webkit-linear-gradient(center bottom, white 20%, #eeeeee 80%); + background-image: -moz-linear-gradient(center bottom, white 20%, #eeeeee 80%); + background-image: -o-linear-gradient(bottom, white 20%, #eeeeee 80%); + background-image: -ms-linear-gradient(top, #ffffff 20%,#eeeeee 80%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); + background-image: linear-gradient(top, #ffffff 20%,#eeeeee 80%); + -webkit-border-bottom-left-radius : 0; + -webkit-border-bottom-right-radius: 0; + -moz-border-radius-bottomleft : 0; + -moz-border-radius-bottomright: 0; + border-bottom-left-radius : 0; + border-bottom-right-radius: 0; +} +.chzn-container-active .chzn-single-with-drop div { + background: transparent; + border-left: none; +} +.chzn-container-active .chzn-single-with-drop div b { + background-position: -18px 1px; +} +.chzn-container-active .chzn-choices { + -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); + -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); + -o-box-shadow : 0 0 5px rgba(0,0,0,.3); + box-shadow : 0 0 5px rgba(0,0,0,.3); + border: 1px solid #5897fb; +} +.chzn-container-active .chzn-choices .search-field input { + color: #111 !important; +} +/* @end */ + +/* @group Disabled Support */ +.chzn-disabled { + cursor: default; + opacity:0.5 !important; +} +.chzn-disabled .chzn-single { + cursor: default; +} +.chzn-disabled .chzn-choices .search-choice .search-choice-close { + cursor: default; +} + +/* @group Right to Left */ +.chzn-rtl { direction:rtl;text-align: right; } +.chzn-rtl .chzn-single { padding-left: 0; padding-right: 8px; } +.chzn-rtl .chzn-single span { margin-left: 26px; margin-right: 0; } + +.chzn-rtl .chzn-single div { left: 3px; right: auto; } +.chzn-rtl .chzn-single abbr { + left: 26px; + right: auto; +} +.chzn-rtl .chzn-choices li { float: right; } +.chzn-rtl .chzn-choices .search-choice { padding: 3px 5px 3px 19px; margin: 3px 5px 3px 0; } +.chzn-rtl .chzn-choices .search-choice .search-choice-close { left: 4px; right: auto; background-position: right top;} +.chzn-rtl.chzn-container-single .chzn-results { margin-left: 4px; margin-right: 0; padding-left: 0; padding-right: 4px; } +.chzn-rtl .chzn-results .group-option { padding-left: 0; padding-right: 20px; } +.chzn-rtl.chzn-container-active .chzn-single-with-drop div { border-right: none; } +.chzn-rtl .chzn-search input { + background: url('../images/chosen-sprite.png') no-repeat -38px -22px, #ffffff; + background: url('../images/chosen-sprite.png') no-repeat -38px -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); + background: url('../images/chosen-sprite.png') no-repeat -38px -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background: url('../images/chosen-sprite.png') no-repeat -38px -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background: url('../images/chosen-sprite.png') no-repeat -38px -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); + background: url('../images/chosen-sprite.png') no-repeat -38px -22px, -ms-linear-gradient(top, #ffffff 85%,#eeeeee 99%); + background: url('../images/chosen-sprite.png') no-repeat -38px -22px, linear-gradient(top, #ffffff 85%,#eeeeee 99%); + padding: 4px 5px 4px 20px; +} +/* @end */ \ No newline at end of file diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 6980e383490..7c182491bd4 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -1240,7 +1240,8 @@ body.package.resource_read #resource-explore { margin-bottom: 2em; } /* = Add Group Page = */ /* ================== */ .group-create-form fieldset#extras, -.group-create-form fieldset#datasets { +.group-create-form fieldset#datasets, +.group-create-form fieldset#users { display: none; } .group-create-form .description-label, diff --git a/ckan/public/images/chosen-sprite.png b/ckan/public/images/chosen-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..231fe9055345db309f56c519b60d1bce144aa095 GIT binary patch literal 1560 zcmeAS@N?(olHy`uVBq!ia0vp^HbAVw!3HE3?VkA`NJ*BsMwA5SrNekFy>6kDZmQ(pt$0_W6>OpmIf)Zi+=kmRcDWXlvKdpiZ23M-%ixv3?I z3Kh9IdBs*0wn|`gt$=Khu)dN4SV>8?trEmh5xxNm&iO^D3Z{Any2%D+1`1||dWOa( z=H}))3PuKo2Koj@`i4fjhUQkrMpgy}3Q(W~w5=#5%__*n4QdyVXRDM^Qc_^0uU}qX zu2*iXmtT~wZ)j<02{OaTNEfI=x41H|B(Xv_uUHvof=g;~a#3bMNoIbY0?5R~r2Ntn zTP2`NAzsKWfE$}v3=Jk=fazBx7U&!58GyV5Q|Rl9UukYGTy=3tP%6T`SPd=?sVqp< z4@xc0FD*(2MqHXQ$f^P>=c3falKi5O{QMkPC;u)9L?MeOic_eEG-QUT@5WPOpPs_4cyEvER398 zEX`ngUGkGlb5rw5V0u#!dQEZa1to>t0-((?6lwduj$h}aM53zTQFZXjzlMeEn$+i%erOI&4kZ z9!mJAEy~mMj#Apy?X=YOV2k0=tEy{xepd)gjoigjVz5iSC(G`7;l9UG-!?5*jpn+3 zwZC0cx3Steu|1*dqF;wC!~A@sLzW8+d;cCkAip75Wrgs)_aE<>9-6w~ZmPk*Cdmy? z6%R!|P_5tzb5!9K^gEgPm*KPCVhi;o+i(ASl9t-4Zr_z?y|r{hQnh;Xhr7xD*Dg5p zZJlJu|NN$cG zGSAyQ&2jCuPmDhfWd4~Q$Xh7ezGZ5A+9jdwU;BXG+UpY)r6jXB{+@}@6}k7@B@0X0 z88oH&r=I;TxX&-UQvXNX2LHL8^2Wszu13i69@h^m_CDch80y)bbpKL9&BL#XE7C13 zCi!fUXWjlFsO7)y>kn@&^^X78wjz_)oF(UZl3VwO-K&' + + '' + + '
' + ui.item.label + '
' + ); + + return false; // to cancel the event ;) + } + }); + }; + + // Attach user autocompletion to provided elements // // Requires: jquery-ui autocomplete diff --git a/ckan/public/scripts/vendor/jquery.chosen/0.9.7/chosen.js b/ckan/public/scripts/vendor/jquery.chosen/0.9.7/chosen.js new file mode 100644 index 00000000000..ef07b3edc58 --- /dev/null +++ b/ckan/public/scripts/vendor/jquery.chosen/0.9.7/chosen.js @@ -0,0 +1,902 @@ +// Chosen, a Select Box Enhancer for jQuery and Protoype +// by Patrick Filler for Harvest, http://getharvest.com +// +// Version 0.9.6 +// Full source at https://github.com/harvesthq/chosen +// Copyright (c) 2011 Harvest http://getharvest.com + +// MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md +// This file is generated by `cake build`, do not edit it by hand. +(function() { + var SelectParser; + SelectParser = (function() { + function SelectParser() { + this.options_index = 0; + this.parsed = []; + } + SelectParser.prototype.add_node = function(child) { + if (child.nodeName === "OPTGROUP") { + return this.add_group(child); + } else { + return this.add_option(child); + } + }; + SelectParser.prototype.add_group = function(group) { + var group_position, option, _i, _len, _ref, _results; + group_position = this.parsed.length; + this.parsed.push({ + array_index: group_position, + group: true, + label: group.label, + children: 0, + disabled: group.disabled + }); + _ref = group.childNodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + option = _ref[_i]; + _results.push(this.add_option(option, group_position, group.disabled)); + } + return _results; + }; + SelectParser.prototype.add_option = function(option, group_position, group_disabled) { + if (option.nodeName === "OPTION") { + if (option.text !== "") { + if (group_position != null) { + this.parsed[group_position].children += 1; + } + this.parsed.push({ + array_index: this.parsed.length, + options_index: this.options_index, + value: option.value, + text: option.text, + html: option.innerHTML, + selected: option.selected, + disabled: group_disabled === true ? group_disabled : option.disabled, + group_array_index: group_position, + classes: option.className, + style: option.style.cssText + }); + } else { + this.parsed.push({ + array_index: this.parsed.length, + options_index: this.options_index, + empty: true + }); + } + return this.options_index += 1; + } + }; + return SelectParser; + })(); + SelectParser.select_to_array = function(select) { + var child, parser, _i, _len, _ref; + parser = new SelectParser(); + _ref = select.childNodes; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + parser.add_node(child); + } + return parser.parsed; + }; + this.SelectParser = SelectParser; +}).call(this); +(function() { + /* + Chosen source: generate output using 'cake build' + Copyright (c) 2011 by Harvest + */ + var AbstractChosen, root; + var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + root = this; + AbstractChosen = (function() { + function AbstractChosen(form_field, options) { + this.form_field = form_field; + this.options = options != null ? options : {}; + this.set_default_values(); + this.is_multiple = this.form_field.multiple; + this.default_text_default = this.is_multiple ? "Select Some Options" : "Select an Option"; + this.setup(); + this.set_up_html(); + this.register_observers(); + this.finish_setup(); + } + AbstractChosen.prototype.set_default_values = function() { + this.click_test_action = __bind(function(evt) { + return this.test_active_click(evt); + }, this); + this.activate_action = __bind(function(evt) { + return this.activate_field(evt); + }, this); + this.active_field = false; + this.mouse_on_container = false; + this.results_showing = false; + this.result_highlighted = null; + this.result_single_selected = null; + this.allow_single_deselect = (this.options.allow_single_deselect != null) && (this.form_field.options[0] != null) && this.form_field.options[0].text === "" ? this.options.allow_single_deselect : false; + this.disable_search_threshold = this.options.disable_search_threshold || 0; + this.choices = 0; + return this.results_none_found = this.options.no_results_text || "No results match"; + }; + AbstractChosen.prototype.mouse_enter = function() { + return this.mouse_on_container = true; + }; + AbstractChosen.prototype.mouse_leave = function() { + return this.mouse_on_container = false; + }; + AbstractChosen.prototype.input_focus = function(evt) { + if (!this.active_field) { + return setTimeout((__bind(function() { + return this.container_mousedown(); + }, this)), 50); + } + }; + AbstractChosen.prototype.input_blur = function(evt) { + if (!this.mouse_on_container) { + this.active_field = false; + return setTimeout((__bind(function() { + return this.blur_test(); + }, this)), 100); + } + }; + AbstractChosen.prototype.result_add_option = function(option) { + var classes, style; + if (!option.disabled) { + option.dom_id = this.container_id + "_o_" + option.array_index; + classes = option.selected && this.is_multiple ? [] : ["active-result"]; + if (option.selected) { + classes.push("result-selected"); + } + if (option.group_array_index != null) { + classes.push("group-option"); + } + if (option.classes !== "") { + classes.push(option.classes); + } + style = option.style.cssText !== "" ? " style=\"" + option.style + "\"" : ""; + return '
  • ' + option.html + '
  • '; + } else { + return ""; + } + }; + AbstractChosen.prototype.results_update_field = function() { + this.result_clear_highlight(); + this.result_single_selected = null; + return this.results_build(); + }; + AbstractChosen.prototype.results_toggle = function() { + if (this.results_showing) { + return this.results_hide(); + } else { + return this.results_show(); + } + }; + AbstractChosen.prototype.results_search = function(evt) { + if (this.results_showing) { + return this.winnow_results(); + } else { + return this.results_show(); + } + }; + AbstractChosen.prototype.keyup_checker = function(evt) { + var stroke, _ref; + stroke = (_ref = evt.which) != null ? _ref : evt.keyCode; + this.search_field_scale(); + switch (stroke) { + case 8: + if (this.is_multiple && this.backstroke_length < 1 && this.choices > 0) { + return this.keydown_backstroke(); + } else if (!this.pending_backstroke) { + this.result_clear_highlight(); + return this.results_search(); + } + break; + case 13: + evt.preventDefault(); + if (this.results_showing) { + return this.result_select(evt); + } + break; + case 27: + if (this.results_showing) { + this.results_hide(); + } + return true; + case 9: + case 38: + case 40: + case 16: + case 91: + case 17: + break; + default: + return this.results_search(); + } + }; + AbstractChosen.prototype.generate_field_id = function() { + var new_id; + new_id = this.generate_random_id(); + this.form_field.id = new_id; + return new_id; + }; + AbstractChosen.prototype.generate_random_char = function() { + var chars, newchar, rand; + chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZ"; + rand = Math.floor(Math.random() * chars.length); + return newchar = chars.substring(rand, rand + 1); + }; + return AbstractChosen; + })(); + root.AbstractChosen = AbstractChosen; +}).call(this); +(function() { + /* + Chosen source: generate output using 'cake build' + Copyright (c) 2011 by Harvest + */ + var $, Chosen, get_side_border_padding, root; + var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { + for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor; + child.__super__ = parent.prototype; + return child; + }, __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + root = this; + $ = jQuery; + $.fn.extend({ + chosen: function(options) { + if ($.browser.msie && ($.browser.version === "6.0" || $.browser.version === "7.0")) { + return this; + } + return $(this).each(function(input_field) { + if (!($(this)).hasClass("chzn-done")) { + return new Chosen(this, options); + } + }); + } + }); + Chosen = (function() { + __extends(Chosen, AbstractChosen); + function Chosen() { + Chosen.__super__.constructor.apply(this, arguments); + } + Chosen.prototype.setup = function() { + this.form_field_jq = $(this.form_field); + return this.is_rtl = this.form_field_jq.hasClass("chzn-rtl"); + }; + Chosen.prototype.finish_setup = function() { + return this.form_field_jq.addClass("chzn-done"); + }; + Chosen.prototype.set_up_html = function() { + var container_div, dd_top, dd_width, sf_width; + this.container_id = this.form_field.id.length ? this.form_field.id.replace(/(:|\.)/g, '_') : this.generate_field_id(); + this.container_id += "_chzn"; + this.f_width = this.form_field_jq.outerWidth(); + this.default_text = this.form_field_jq.data('placeholder') ? this.form_field_jq.data('placeholder') : this.default_text_default; + container_div = $("
    ", { + id: this.container_id, + "class": "chzn-container" + (this.is_rtl ? ' chzn-rtl' : ''), + style: 'width: ' + this.f_width + 'px;' + }); + if (this.is_multiple) { + container_div.html('
      '); + } else { + container_div.html('' + this.default_text + '
        '); + } + this.form_field_jq.hide().after(container_div); + this.container = $('#' + this.container_id); + this.container.addClass("chzn-container-" + (this.is_multiple ? "multi" : "single")); + this.dropdown = this.container.find('div.chzn-drop').first(); + dd_top = this.container.height(); + dd_width = this.f_width - get_side_border_padding(this.dropdown); + this.dropdown.css({ + "width": dd_width + "px", + "top": dd_top + "px" + }); + this.search_field = this.container.find('input').first(); + this.search_results = this.container.find('ul.chzn-results').first(); + this.search_field_scale(); + this.search_no_results = this.container.find('li.no-results').first(); + if (this.is_multiple) { + this.search_choices = this.container.find('ul.chzn-choices').first(); + this.search_container = this.container.find('li.search-field').first(); + } else { + this.search_container = this.container.find('div.chzn-search').first(); + this.selected_item = this.container.find('.chzn-single').first(); + sf_width = dd_width - get_side_border_padding(this.search_container) - get_side_border_padding(this.search_field); + this.search_field.css({ + "width": sf_width + "px" + }); + } + this.results_build(); + this.set_tab_index(); + return this.form_field_jq.trigger("liszt:ready", { + chosen: this + }); + }; + Chosen.prototype.register_observers = function() { + this.container.mousedown(__bind(function(evt) { + return this.container_mousedown(evt); + }, this)); + this.container.mouseup(__bind(function(evt) { + return this.container_mouseup(evt); + }, this)); + this.container.mouseenter(__bind(function(evt) { + return this.mouse_enter(evt); + }, this)); + this.container.mouseleave(__bind(function(evt) { + return this.mouse_leave(evt); + }, this)); + this.search_results.mouseup(__bind(function(evt) { + return this.search_results_mouseup(evt); + }, this)); + this.search_results.mouseover(__bind(function(evt) { + return this.search_results_mouseover(evt); + }, this)); + this.search_results.mouseout(__bind(function(evt) { + return this.search_results_mouseout(evt); + }, this)); + this.form_field_jq.bind("liszt:updated", __bind(function(evt) { + return this.results_update_field(evt); + }, this)); + this.search_field.blur(__bind(function(evt) { + return this.input_blur(evt); + }, this)); + this.search_field.keyup(__bind(function(evt) { + return this.keyup_checker(evt); + }, this)); + this.search_field.keydown(__bind(function(evt) { + return this.keydown_checker(evt); + }, this)); + if (this.is_multiple) { + this.search_choices.click(__bind(function(evt) { + return this.choices_click(evt); + }, this)); + return this.search_field.focus(__bind(function(evt) { + return this.input_focus(evt); + }, this)); + } else { + return this.container.click(__bind(function(evt) { + return evt.preventDefault(); + }, this)); + } + }; + Chosen.prototype.search_field_disabled = function() { + this.is_disabled = this.form_field_jq[0].disabled; + if (this.is_disabled) { + this.container.addClass('chzn-disabled'); + this.search_field[0].disabled = true; + if (!this.is_multiple) { + this.selected_item.unbind("focus", this.activate_action); + } + return this.close_field(); + } else { + this.container.removeClass('chzn-disabled'); + this.search_field[0].disabled = false; + if (!this.is_multiple) { + return this.selected_item.bind("focus", this.activate_action); + } + } + }; + Chosen.prototype.container_mousedown = function(evt) { + var target_closelink; + if (!this.is_disabled) { + target_closelink = evt != null ? ($(evt.target)).hasClass("search-choice-close") : false; + if (evt && evt.type === "mousedown") { + evt.stopPropagation(); + } + if (!this.pending_destroy_click && !target_closelink) { + if (!this.active_field) { + if (this.is_multiple) { + this.search_field.val(""); + } + $(document).click(this.click_test_action); + this.results_show(); + } else if (!this.is_multiple && evt && (($(evt.target)[0] === this.selected_item[0]) || $(evt.target).parents("a.chzn-single").length)) { + evt.preventDefault(); + this.results_toggle(); + } + return this.activate_field(); + } else { + return this.pending_destroy_click = false; + } + } + }; + Chosen.prototype.container_mouseup = function(evt) { + if (evt.target.nodeName === "ABBR") { + return this.results_reset(evt); + } + }; + Chosen.prototype.blur_test = function(evt) { + if (!this.active_field && this.container.hasClass("chzn-container-active")) { + return this.close_field(); + } + }; + Chosen.prototype.close_field = function() { + $(document).unbind("click", this.click_test_action); + if (!this.is_multiple) { + this.selected_item.attr("tabindex", this.search_field.attr("tabindex")); + this.search_field.attr("tabindex", -1); + } + this.active_field = false; + this.results_hide(); + this.container.removeClass("chzn-container-active"); + this.winnow_results_clear(); + this.clear_backstroke(); + this.show_search_field_default(); + return this.search_field_scale(); + }; + Chosen.prototype.activate_field = function() { + if (!this.is_multiple && !this.active_field) { + this.search_field.attr("tabindex", this.selected_item.attr("tabindex")); + this.selected_item.attr("tabindex", -1); + } + this.container.addClass("chzn-container-active"); + this.active_field = true; + this.search_field.val(this.search_field.val()); + return this.search_field.focus(); + }; + Chosen.prototype.test_active_click = function(evt) { + if ($(evt.target).parents('#' + this.container_id).length) { + return this.active_field = true; + } else { + return this.close_field(); + } + }; + Chosen.prototype.results_build = function() { + var content, data, _i, _len, _ref; + this.parsing = true; + this.results_data = root.SelectParser.select_to_array(this.form_field); + if (this.is_multiple && this.choices > 0) { + this.search_choices.find("li.search-choice").remove(); + this.choices = 0; + } else if (!this.is_multiple) { + this.selected_item.find("span").text(this.default_text); + if (this.form_field.options.length <= this.disable_search_threshold) { + this.container.addClass("chzn-container-single-nosearch"); + } else { + this.container.removeClass("chzn-container-single-nosearch"); + } + } + content = ''; + _ref = this.results_data; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + data = _ref[_i]; + if (data.group) { + content += this.result_add_group(data); + } else if (!data.empty) { + content += this.result_add_option(data); + if (data.selected && this.is_multiple) { + this.choice_build(data); + } else if (data.selected && !this.is_multiple) { + this.selected_item.find("span").text(data.text); + if (this.allow_single_deselect) { + this.single_deselect_control_build(); + } + } + } + } + this.search_field_disabled(); + this.show_search_field_default(); + this.search_field_scale(); + this.search_results.html(content); + return this.parsing = false; + }; + Chosen.prototype.result_add_group = function(group) { + if (!group.disabled) { + group.dom_id = this.container_id + "_g_" + group.array_index; + return '
      • ' + $("
        ").text(group.label).html() + '
      • '; + } else { + return ""; + } + }; + Chosen.prototype.result_do_highlight = function(el) { + var high_bottom, high_top, maxHeight, visible_bottom, visible_top; + if (el.length) { + this.result_clear_highlight(); + this.result_highlight = el; + this.result_highlight.addClass("highlighted"); + maxHeight = parseInt(this.search_results.css("maxHeight"), 10); + visible_top = this.search_results.scrollTop(); + visible_bottom = maxHeight + visible_top; + high_top = this.result_highlight.position().top + this.search_results.scrollTop(); + high_bottom = high_top + this.result_highlight.outerHeight(); + if (high_bottom >= visible_bottom) { + return this.search_results.scrollTop((high_bottom - maxHeight) > 0 ? high_bottom - maxHeight : 0); + } else if (high_top < visible_top) { + return this.search_results.scrollTop(high_top); + } + } + }; + Chosen.prototype.result_clear_highlight = function() { + if (this.result_highlight) { + this.result_highlight.removeClass("highlighted"); + } + return this.result_highlight = null; + }; + Chosen.prototype.results_show = function() { + var dd_top; + if (!this.is_multiple) { + this.selected_item.addClass("chzn-single-with-drop"); + if (this.result_single_selected) { + this.result_do_highlight(this.result_single_selected); + } + } + dd_top = this.is_multiple ? this.container.height() : this.container.height() - 1; + this.dropdown.css({ + "top": dd_top + "px", + "left": 0 + }); + this.results_showing = true; + this.search_field.focus(); + this.search_field.val(this.search_field.val()); + return this.winnow_results(); + }; + Chosen.prototype.results_hide = function() { + if (!this.is_multiple) { + this.selected_item.removeClass("chzn-single-with-drop"); + } + this.result_clear_highlight(); + this.dropdown.css({ + "left": "-9000px" + }); + return this.results_showing = false; + }; + Chosen.prototype.set_tab_index = function(el) { + var ti; + if (this.form_field_jq.attr("tabindex")) { + ti = this.form_field_jq.attr("tabindex"); + this.form_field_jq.attr("tabindex", -1); + if (this.is_multiple) { + return this.search_field.attr("tabindex", ti); + } else { + this.selected_item.attr("tabindex", ti); + return this.search_field.attr("tabindex", -1); + } + } + }; + Chosen.prototype.show_search_field_default = function() { + if (this.is_multiple && this.choices < 1 && !this.active_field) { + this.search_field.val(this.default_text); + return this.search_field.addClass("default"); + } else { + this.search_field.val(""); + return this.search_field.removeClass("default"); + } + }; + Chosen.prototype.search_results_mouseup = function(evt) { + var target; + target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first(); + if (target.length) { + this.result_highlight = target; + return this.result_select(evt); + } + }; + Chosen.prototype.search_results_mouseover = function(evt) { + var target; + target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first(); + if (target) { + return this.result_do_highlight(target); + } + }; + Chosen.prototype.search_results_mouseout = function(evt) { + if ($(evt.target).hasClass("active-result" || $(evt.target).parents('.active-result').first())) { + return this.result_clear_highlight(); + } + }; + Chosen.prototype.choices_click = function(evt) { + evt.preventDefault(); + if (this.active_field && !($(evt.target).hasClass("search-choice" || $(evt.target).parents('.search-choice').first)) && !this.results_showing) { + return this.results_show(); + } + }; + Chosen.prototype.choice_build = function(item) { + var choice_id, link; + choice_id = this.container_id + "_c_" + item.array_index; + this.choices += 1; + this.search_container.before('
      • ' + item.html + '
      • '); + link = $('#' + choice_id).find("a").first(); + return link.click(__bind(function(evt) { + return this.choice_destroy_link_click(evt); + }, this)); + }; + Chosen.prototype.choice_destroy_link_click = function(evt) { + evt.preventDefault(); + if (!this.is_disabled) { + this.pending_destroy_click = true; + return this.choice_destroy($(evt.target)); + } else { + return evt.stopPropagation; + } + }; + Chosen.prototype.choice_destroy = function(link) { + this.choices -= 1; + this.show_search_field_default(); + if (this.is_multiple && this.choices > 0 && this.search_field.val().length < 1) { + this.results_hide(); + } + this.result_deselect(link.attr("rel")); + return link.parents('li').first().remove(); + }; + Chosen.prototype.results_reset = function(evt) { + this.form_field.options[0].selected = true; + this.selected_item.find("span").text(this.default_text); + this.show_search_field_default(); + $(evt.target).remove(); + this.form_field_jq.trigger("change"); + if (this.active_field) { + return this.results_hide(); + } + }; + Chosen.prototype.result_select = function(evt) { + var high, high_id, item, position; + if (this.result_highlight) { + high = this.result_highlight; + high_id = high.attr("id"); + this.result_clear_highlight(); + if (this.is_multiple) { + this.result_deactivate(high); + } else { + this.search_results.find(".result-selected").removeClass("result-selected"); + this.result_single_selected = high; + } + high.addClass("result-selected"); + position = high_id.substr(high_id.lastIndexOf("_") + 1); + item = this.results_data[position]; + item.selected = true; + this.form_field.options[item.options_index].selected = true; + if (this.is_multiple) { + this.choice_build(item); + } else { + this.selected_item.find("span").first().text(item.text); + if (this.allow_single_deselect) { + this.single_deselect_control_build(); + } + } + if (!(evt.metaKey && this.is_multiple)) { + this.results_hide(); + } + this.search_field.val(""); + this.form_field_jq.trigger("change"); + return this.search_field_scale(); + } + }; + Chosen.prototype.result_activate = function(el) { + return el.addClass("active-result"); + }; + Chosen.prototype.result_deactivate = function(el) { + return el.removeClass("active-result"); + }; + Chosen.prototype.result_deselect = function(pos) { + var result, result_data; + result_data = this.results_data[pos]; + result_data.selected = false; + this.form_field.options[result_data.options_index].selected = false; + result = $("#" + this.container_id + "_o_" + pos); + result.removeClass("result-selected").addClass("active-result").show(); + this.result_clear_highlight(); + this.winnow_results(); + this.form_field_jq.trigger("change"); + return this.search_field_scale(); + }; + Chosen.prototype.single_deselect_control_build = function() { + if (this.allow_single_deselect && this.selected_item.find("abbr").length < 1) { + return this.selected_item.find("span").first().after(""); + } + }; + Chosen.prototype.winnow_results = function() { + var found, option, part, parts, regex, result, result_id, results, searchText, startpos, text, zregex, _i, _j, _len, _len2, _ref; + this.no_results_clear(); + results = 0; + searchText = this.search_field.val() === this.default_text ? "" : $('
        ').text($.trim(this.search_field.val())).html(); + regex = new RegExp('^' + searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i'); + zregex = new RegExp(searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i'); + _ref = this.results_data; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + option = _ref[_i]; + if (!option.disabled && !option.empty) { + if (option.group) { + $('#' + option.dom_id).css('display', 'none'); + } else if (!(this.is_multiple && option.selected)) { + found = false; + result_id = option.dom_id; + result = $("#" + result_id); + if (regex.test(option.html)) { + found = true; + results += 1; + } else if (option.html.indexOf(" ") >= 0 || option.html.indexOf("[") === 0) { + parts = option.html.replace(/\[|\]/g, "").split(" "); + if (parts.length) { + for (_j = 0, _len2 = parts.length; _j < _len2; _j++) { + part = parts[_j]; + if (regex.test(part)) { + found = true; + results += 1; + } + } + } + } + if (found) { + if (searchText.length) { + startpos = option.html.search(zregex); + text = option.html.substr(0, startpos + searchText.length) + '' + option.html.substr(startpos + searchText.length); + text = text.substr(0, startpos) + '' + text.substr(startpos); + } else { + text = option.html; + } + result.html(text); + this.result_activate(result); + if (option.group_array_index != null) { + $("#" + this.results_data[option.group_array_index].dom_id).css('display', 'list-item'); + } + } else { + if (this.result_highlight && result_id === this.result_highlight.attr('id')) { + this.result_clear_highlight(); + } + this.result_deactivate(result); + } + } + } + } + if (results < 1 && searchText.length) { + return this.no_results(searchText); + } else { + return this.winnow_results_set_highlight(); + } + }; + Chosen.prototype.winnow_results_clear = function() { + var li, lis, _i, _len, _results; + this.search_field.val(""); + lis = this.search_results.find("li"); + _results = []; + for (_i = 0, _len = lis.length; _i < _len; _i++) { + li = lis[_i]; + li = $(li); + _results.push(li.hasClass("group-result") ? li.css('display', 'auto') : !this.is_multiple || !li.hasClass("result-selected") ? this.result_activate(li) : void 0); + } + return _results; + }; + Chosen.prototype.winnow_results_set_highlight = function() { + var do_high, selected_results; + if (!this.result_highlight) { + selected_results = !this.is_multiple ? this.search_results.find(".result-selected.active-result") : []; + do_high = selected_results.length ? selected_results.first() : this.search_results.find(".active-result").first(); + if (do_high != null) { + return this.result_do_highlight(do_high); + } + } + }; + Chosen.prototype.no_results = function(terms) { + var no_results_html; + no_results_html = $('
      • ' + this.results_none_found + ' ""
      • '); + no_results_html.find("span").first().html(terms); + return this.search_results.append(no_results_html); + }; + Chosen.prototype.no_results_clear = function() { + return this.search_results.find(".no-results").remove(); + }; + Chosen.prototype.keydown_arrow = function() { + var first_active, next_sib; + if (!this.result_highlight) { + first_active = this.search_results.find("li.active-result").first(); + if (first_active) { + this.result_do_highlight($(first_active)); + } + } else if (this.results_showing) { + next_sib = this.result_highlight.nextAll("li.active-result").first(); + if (next_sib) { + this.result_do_highlight(next_sib); + } + } + if (!this.results_showing) { + return this.results_show(); + } + }; + Chosen.prototype.keyup_arrow = function() { + var prev_sibs; + if (!this.results_showing && !this.is_multiple) { + return this.results_show(); + } else if (this.result_highlight) { + prev_sibs = this.result_highlight.prevAll("li.active-result"); + if (prev_sibs.length) { + return this.result_do_highlight(prev_sibs.first()); + } else { + if (this.choices > 0) { + this.results_hide(); + } + return this.result_clear_highlight(); + } + } + }; + Chosen.prototype.keydown_backstroke = function() { + if (this.pending_backstroke) { + this.choice_destroy(this.pending_backstroke.find("a").first()); + return this.clear_backstroke(); + } else { + this.pending_backstroke = this.search_container.siblings("li.search-choice").last(); + return this.pending_backstroke.addClass("search-choice-focus"); + } + }; + Chosen.prototype.clear_backstroke = function() { + if (this.pending_backstroke) { + this.pending_backstroke.removeClass("search-choice-focus"); + } + return this.pending_backstroke = null; + }; + Chosen.prototype.keydown_checker = function(evt) { + var stroke, _ref; + stroke = (_ref = evt.which) != null ? _ref : evt.keyCode; + this.search_field_scale(); + if (stroke !== 8 && this.pending_backstroke) { + this.clear_backstroke(); + } + switch (stroke) { + case 8: + this.backstroke_length = this.search_field.val().length; + break; + case 9: + if (this.results_showing && !this.is_multiple) { + this.result_select(evt); + } + this.mouse_on_container = false; + break; + case 13: + evt.preventDefault(); + break; + case 38: + evt.preventDefault(); + this.keyup_arrow(); + break; + case 40: + this.keydown_arrow(); + break; + } + }; + Chosen.prototype.search_field_scale = function() { + var dd_top, div, h, style, style_block, styles, w, _i, _len; + if (this.is_multiple) { + h = 0; + w = 0; + style_block = "position:absolute; left: -1000px; top: -1000px; display:none;"; + styles = ['font-size', 'font-style', 'font-weight', 'font-family', 'line-height', 'text-transform', 'letter-spacing']; + for (_i = 0, _len = styles.length; _i < _len; _i++) { + style = styles[_i]; + style_block += style + ":" + this.search_field.css(style) + ";"; + } + div = $('
        ', { + 'style': style_block + }); + div.text(this.search_field.val()); + $('body').append(div); + w = div.width() + 25; + div.remove(); + if (w > this.f_width - 10) { + w = this.f_width - 10; + } + this.search_field.css({ + 'width': w + 'px' + }); + dd_top = this.container.height(); + return this.dropdown.css({ + "top": dd_top + "px" + }); + } + }; + Chosen.prototype.generate_random_id = function() { + var string; + string = "sel" + this.generate_random_char() + this.generate_random_char() + this.generate_random_char(); + while ($("#" + string).length > 0) { + string += this.generate_random_char(); + } + return string; + }; + return Chosen; + })(); + get_side_border_padding = function(elmt) { + var side_border_padding; + return side_border_padding = elmt.outerWidth() - elmt.width(); + }; + root.get_side_border_padding = get_side_border_padding; +}).call(this); \ No newline at end of file diff --git a/ckan/templates/layout_base.html b/ckan/templates/layout_base.html index ca23a0c9a94..7db677b2957 100644 --- a/ckan/templates/layout_base.html +++ b/ckan/templates/layout_base.html @@ -31,6 +31,7 @@ + @@ -250,6 +252,8 @@

        Meta

        $(".ckan-logged-in").show(); } $('input[placeholder], textarea[placeholder]').placeholder(); + + $(".chzn-select").chosen(); }); diff --git a/ckanext/publisher_form/__init__.py b/ckanext/publisher_form/__init__.py new file mode 100644 index 00000000000..37a0ca0ec88 --- /dev/null +++ b/ckanext/publisher_form/__init__.py @@ -0,0 +1,6 @@ +'''Publisher form + +Provides a form for publisher creation +''' +__version__ = '0.1' + diff --git a/ckanext/publisher_form/forms.py b/ckanext/publisher_form/forms.py new file mode 100644 index 00000000000..79a687d744d --- /dev/null +++ b/ckanext/publisher_form/forms.py @@ -0,0 +1,112 @@ +import os, logging +from ckan.authz import Authorizer +import ckan.logic.action.create as create +import ckan.logic.action.update as update +import ckan.logic.action.get as get +from ckan.logic.converters import date_to_db, date_to_form, convert_to_extras, convert_from_extras +from ckan.logic import NotFound, NotAuthorized, ValidationError +from ckan.logic import tuplize_dict, clean_dict, parse_params +import ckan.logic.schema as default_schema +from ckan.logic.schema import group_form_schema +from ckan.logic.schema import package_form_schema +import ckan.logic.validators as val +from ckan.lib.base import BaseController, render, c, model, abort, request +from ckan.lib.base import redirect, _, config, h +from ckan.lib.package_saver import PackageSaver +from ckan.lib.field_types import DateType, DateConvertError +from ckan.lib.navl.dictization_functions import Invalid +from ckan.lib.navl.dictization_functions import validate, missing +from ckan.lib.navl.dictization_functions import DataError, flatten_dict, unflatten +from ckan.plugins import IDatasetForm, IGroupForm, IConfigurer +from ckan.plugins import implements, SingletonPlugin + +from ckan.lib.navl.validators import (ignore_missing, + not_empty, + empty, + ignore, + keep_extras, + ) + +log = logging.getLogger(__name__) + +class PublisherForm(SingletonPlugin): + """ + This plugin implements an IGroupForm for form associated with a + publisher group. ``IConfigurer`` is used to add the local template + path and the IGroupForm supplies the custom form. + """ + implements(IGroupForm, inherit=True) + implements(IConfigurer, inherit=True) + + def update_config(self, config): + """ + This IConfigurer implementation causes CKAN to look in the + ```templates``` directory when looking for the group_form() + """ + here = os.path.dirname(__file__) + rootdir = os.path.dirname(os.path.dirname(here)) + template_dir = os.path.join(rootdir, 'ckanext', + 'publisher_form', 'templates') + config['extra_template_paths'] = ','.join([template_dir, + config.get('extra_template_paths', '')]) + + def group_form(self): + """ + Returns a string representing the location of the template to be + rendered. e.g. "forms/group_form.html". + """ + return 'publisher_form.html' + + def group_types(self): + """ + Returns an iterable of group type strings. + + If a request involving a group of one of those types is made, then + this plugin instance will be delegated to. + + There must only be one plugin registered to each group type. Any + attempts to register more than one plugin instance to a given group + type will raise an exception at startup. + """ + return ["publisher"] + + def is_fallback(self): + """ + Returns true iff this provides the fallback behaviour, when no other + plugin instance matches a group's type. + + As this is not the fallback controller we should return False. If + we were wanting to act as the fallback, we'd return True + """ + return False + + def form_to_db_schema(self): + """ + Returns the schema for mapping group data from a form to a format + suitable for the database. + """ + return group_form_schema() + + def db_to_form_schema(self): + """ + Returns the schema for mapping group data from the database into a + format suitable for the form (optional) + """ + return {} + + def check_data_dict(self, data_dict): + """ + Check if the return data is correct. + + raise a DataError if not. + """ + + def setup_template_variables(self, context, data_dict): + """ + Add variables to c just prior to the template being rendered. We should + use the available groups for the current user, but should be optional + in case this is a top level group + """ + #c.user_groups = c.userobj.get_groups('publisher') + c.user_groups = ['One', 'Two', 'Three'] + \ No newline at end of file diff --git a/ckanext/publisher_form/templates/publisher_form.html b/ckanext/publisher_form/templates/publisher_form.html new file mode 100644 index 00000000000..63450d2489a --- /dev/null +++ b/ckanext/publisher_form/templates/publisher_form.html @@ -0,0 +1,135 @@ + +
        + +
        +

        Errors in form

        +

        The form contains invalid entries:

        +
          +
        • ${"%s: %s" % (key, error)}
        • +
        +
        + + + +
        +
        +
        +
        + + +
        +
        + ${g.site_url+h.url_for('publisher_index')+'/'}  + +

         

        +
        + +
        ${errors.get('name', '')}
        + +
        +
        +
          +
        • +
        • +
        + + + You can use Markdown formatting here. +
        + + +
        +
        + +
        +
        +
        + +
        +

        Extras

        +
        + + +
        +
        + + + Delete +
        +
        + + +
        +
        + + with value + +
        +
        +
        +
        +
        + +
        +

        Users

        +
        + +
        +
        +
        + +
        +
        + Admin + Editor +
        +
        +
        +

        There are no users currently in this publisher.

        + + +

        Add users

        +
        +
        +
        + Admin + Editor +
        +
        +
        +
        + + +
        +

        Datasets

        +
        + +
        +
        + +
        +
        +
        +

        There are no datasets currently in this publisher.

        + +

        Add datasets

        +
        +
        +
        +
        +
        + +
        + + + + +
        +
        diff --git a/setup.py b/setup.py index a0ef5ae60cc..0d949caf367 100644 --- a/setup.py +++ b/setup.py @@ -86,6 +86,7 @@ [ckan.plugins] synchronous_search = ckan.lib.search:SynchronousSearchPlugin stats=ckanext.stats.plugin:StatsPlugin + publisher_form=ckanext.publisher_form.forms:PublisherForm [ckan.system_plugins] domain_object_mods = ckan.model.modification:DomainObjectModificationExtension From c88c9003cef23ef8718e5d70d8153ee82192c249 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 30 Jan 2012 14:33:35 +0000 Subject: [PATCH 15/59] [1669] Changes to auth to enable users to add datasets to their group --- ckan/controllers/group.py | 2 +- ckan/controllers/package.py | 11 +- ckan/lib/create_test_data.py | 9 +- ckan/lib/dictization/model_save.py | 17 +- ckan/logic/action/create.py | 4 +- ckan/logic/auth/publisher/create.py | 13 +- ckan/logic/auth/publisher/update.py | 17 +- ckan/model/group.py | 1 - ckan/templates/group/layout.html | 5 +- ckan/tests/functional/api/__init__.py | 2 +- .../api/model/test_relationships.py | 6 +- ckan/tests/functional/api/test_action.py | 10 +- ckan/tests/misc/test_auth_profiles.py | 3 +- ckanext/publisher_form/forms.py | 91 +++++++ .../templates/dataset_form.html | 231 ++++++++++++++++++ setup.py | 1 + 16 files changed, 366 insertions(+), 57 deletions(-) create mode 100644 ckanext/publisher_form/templates/dataset_form.html diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 1260c3f6db8..ad1ad8a5e60 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -301,7 +301,6 @@ def pager_url(q=None, page=None): return render('group/read.html') def new(self, data=None, errors=None, error_summary=None): - group_type = request.path.strip('/').split('/')[0] if group_type == 'group': group_type = None @@ -401,6 +400,7 @@ def _save_new(self, context, group_type=None): tuplize_dict(parse_params(request.params)))) data_dict['type'] = group_type or 'group' context['message'] = data_dict.get('log_message', '') + data_dict['users'] = [{'name': c.user, 'capacity': 'admin'}] group = get_action('group_create')(context, data_dict) # Redirect to the appropriate _read route for the type of group diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index a455af60d53..1f0c5fe801d 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -67,15 +67,9 @@ def register_pluggable_behaviour(map): """ global _default_controller_behaviour - # Check this method hasn't been invoked already. - # TODO: This method seems to be being invoked more than once during running of - # the tests. So I've disbabled this check until I figure out why. - #if _default_controller_behaviour is not None: - #raise ValueError, "Pluggable package controller behaviour is already defined "\ - #"'%s'" % _default_controller_behaviour - # Create the mappings and register the fallback behaviour if one is found. for plugin in PluginImplementations(IDatasetForm): + print 'Processing %r' % plugin if plugin.is_fallback(): if _default_controller_behaviour is not None: raise ValueError, "More than one fallback "\ @@ -84,7 +78,6 @@ def register_pluggable_behaviour(map): for package_type in plugin.package_types(): # Create a connection between the newly named type and the package controller - # but first we need to make sure we are not clobbering an existing domain map.connect('/%s/new' % (package_type,), controller='package', action='new') map.connect('%s_read' % (package_type,), '/%s/{id}' % (package_type,), controller='package', action='read') map.connect('%s_action' % (package_type,), @@ -440,6 +433,8 @@ def new(self, data=None, errors=None, error_summary=None): 'save': 'save' in request.params, 'schema': self._form_to_db_schema(package_type=package_type)} + # Package needs to have a publisher group in the call to check_access + # and also to save it try: check_access('package_create',context) except NotAuthorized: diff --git a/ckan/lib/create_test_data.py b/ckan/lib/create_test_data.py index 7804f96bd1f..69918b86867 100644 --- a/ckan/lib/create_test_data.py +++ b/ckan/lib/create_test_data.py @@ -319,15 +319,10 @@ def create(cls): * Associated tags, etc etc ''' - publisher_group = model.Group(name=u'default_publisher', - title=u'Default publisher', - description=u'The default test publisher', - type='publisher') - model.Session.add(publisher_group) cls.pkg_names = [u'annakarenina', u'warandpeace'] pkg1 = model.Package(name=cls.pkg_names[0]) - pkg1.group = publisher_group + #pkg1.group = publisher_group model.Session.add(pkg1) pkg1.title = u'A Novel By Tolstoy' pkg1.version = u'0.7a' @@ -381,7 +376,7 @@ def create(cls): tag1 = model.Tag(name=u'russian') tag2 = model.Tag(name=u'tolstoy') - pkg2.group = publisher_group + #pkg2.group = publisher_group # Flexible tag, allows spaces, upper-case, # and all punctuation except commas diff --git a/ckan/lib/dictization/model_save.py b/ckan/lib/dictization/model_save.py index 3dcb5bb00c3..b8a0d071fc1 100644 --- a/ckan/lib/dictization/model_save.py +++ b/ckan/lib/dictization/model_save.py @@ -213,6 +213,7 @@ def package_membership_list_save(group_dicts, package, context): member_obj = model.Member(table_id = package.id, table_name = 'package', group = group, + group_id=group.id, state = 'active') session.add(member_obj) @@ -256,7 +257,8 @@ def relationship_list_save(relationship_dicts, package, attr, context): relationship_list.append(relationship) def package_dict_save(pkg_dict, context): - + import uuid + model = context["model"] package = context.get("package") allow_partial_update = context.get("allow_partial_update", False) @@ -271,6 +273,9 @@ def package_dict_save(pkg_dict, context): pkg = table_dict_save(pkg_dict, Package, context) + if not pkg.id: + pkg.id = str(uuid.uuid4()) + package_resource_list_save(pkg_dict.get("resources", []), pkg, context) package_tag_list_save(pkg_dict.get("tags", []), pkg, context) package_membership_list_save(pkg_dict.get("groups", []), pkg, context) @@ -316,14 +321,15 @@ def group_member_save(context, group_dict, member_table_name): session.add(entity_member[entity_id]) for entity_id in set(entities.keys()) - set(entity_member.keys()): - member = Member(group=group, table_id=entity_id[0], + member = Member(group=group, group_id=group.id, table_id=entity_id[0], table_name=member_table_name[:-1], capacity=entity_id[1]) session.add(member) def group_dict_save(group_dict, context): - + import uuid + model = context["model"] session = context["session"] group = context.get("group") @@ -334,8 +340,11 @@ def group_dict_save(group_dict, context): group_dict["id"] = group.id group = table_dict_save(group_dict, Group, context) + if not group.id: + group.id = str(uuid.uuid4()) + context['group'] = group - print group_dict + group_member_save(context, group_dict, 'packages') group_member_save(context, group_dict, 'users') group_member_save(context, group_dict, 'groups') diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index 423afbc2192..c970d779034 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -172,8 +172,7 @@ def group_create(context, data_dict): rev.message = _(u'REST API: Create object %s') % data.get("name") group = group_dict_save(data, context) - - # TODO: Refactor to use new member for admin + if user: admins = [model.User.by_name(user.decode('utf8'))] else: @@ -181,6 +180,7 @@ def group_create(context, data_dict): model.setup_default_user_roles(group, admins) # Needed to let extensions know the group id model.Session.flush() + for item in PluginImplementations(IGroupController): item.create(group) if not context.get('defer_commit'): diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index cddb9f77e18..7cd515ca600 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -1,7 +1,7 @@ from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ get_user_object, get_resource_object from ckan.logic.auth.publisher import _groups_intersect -from ckan.logic import check_access_old, NotFound +from ckan.logic import NotFound from ckan.authz import Authorizer from ckan.lib.base import _ @@ -10,17 +10,6 @@ def package_create(context, data_dict=None): model = context['model'] user = context['user'] - # We need the publisher group passed in as part of this request - try: - group = get_group_object( context ) - except NotFound: - return {'success': False, - 'msg': _('User %s not authorized to create a package without a group specified') % str(user)} - - userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups('publisher'), group.get_groups('publisher') ): - return {'success': False, 'msg': _('User %s not authorized to create a package here') % str(user)} - return {'success': True} def resource_create(context, data_dict): diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index ed77302c558..d8da07f409c 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -12,12 +12,13 @@ def package_update(context, data_dict): model = context['model'] user = context.get('user') package = get_package_object(context, data_dict) - - userobj = model.User.get( user ) - if not userobj or \ - not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): - return {'success': False, - 'msg': _('User %s not authorized to edit packages in these groups') % str(user)} +# group = get_group_object( context, data_dict ) + +# userobj = model.User.get( user ) +# if not userobj or \ +# not _groups_intersect( userobj.get_groups('publisher'), [group] ): +# return {'success': False, +# 'msg': _('User %s not authorized to edit packages in these groups') % str(user)} return {'success': True} @@ -46,10 +47,10 @@ def group_update(context, data_dict): model = context['model'] user = context['user'] group = get_group_object(context, data_dict) - + # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) - if not _groups_intersect( userobj.get_groups('publisher', 'admin'), group.get_groups('publisher') ): + if not _groups_intersect( userobj.get_groups('publisher', 'admin'), [group] ): return {'success': False, 'msg': _('User %s not authorized to edit this group') % str(user)} return {'success': True} diff --git a/ckan/model/group.py b/ckan/model/group.py index 47c82ffb677..08941fd2286 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -130,7 +130,6 @@ def as_dict(self, ref_package_by='name'): return _dict def add_package_by_name(self, package_name): - from pdb import set_trace; set_trace() if not package_name: return package = Package.by_name(package_name) diff --git a/ckan/templates/group/layout.html b/ckan/templates/group/layout.html index a830aaf6136..403d6a5582d 100644 --- a/ckan/templates/group/layout.html +++ b/ckan/templates/group/layout.html @@ -26,7 +26,6 @@
      • ${h.subnav_named_route( c,h.icon('group_edit') + _('Edit'), c.group.type + '_action', action='edit', id=c.group.name )} - ${h.subnav_link(c, h.icon('group_edit') + _('Edit'), controller='group', action='edit', id=c.group.name)}
      • ${h.subnav_named_route(c, h.icon('lock') + _('Authorization'), c.group.type + '_action', controller='group', action='authz', id=c.group.name)} @@ -42,8 +41,8 @@ ${h.subnav_link(c, h.icon('group') + _('List Groups'), controller='group', action='index')}
      • - - ${h.subnav_link(c, h.icon('group_add') + _('Login to Add a Group'), controller='group', action='new')} + + ${h.subnav_link(c, h.icon('group_add') + _('Login to Add a Publisher'), controller='group', action='new')}
      • diff --git a/ckan/tests/functional/api/__init__.py b/ckan/tests/functional/api/__init__.py index 208b40157d3..1c6e2442feb 100644 --- a/ckan/tests/functional/api/__init__.py +++ b/ckan/tests/functional/api/__init__.py @@ -31,5 +31,5 @@ def assert_dicts_equal_ignoring_ordering(dict1, dict2): dicts = [copy.deepcopy(dict1), copy.deepcopy(dict2)] for d in dicts: d = change_lists_to_sets(d) - from nose.tools import set_trace; set_trace() + #from nose.tools import set_trace; set_trace() assert_equal(dicts[0], dicts[1]) diff --git a/ckan/tests/functional/api/model/test_relationships.py b/ckan/tests/functional/api/model/test_relationships.py index 77aadc86ccc..86481bd6592 100644 --- a/ckan/tests/functional/api/model/test_relationships.py +++ b/ckan/tests/functional/api/model/test_relationships.py @@ -255,9 +255,9 @@ def test_update_relationship_incorrectly(self): res = self.app.put(offset, params=postparams, status=[200], extra_environ=self.extra_environ) print res.body - assert '123' not in res.body - assert '456' not in res.body - assert '789' in res.body + assert '123' not in res.body, res.body + assert '456' not in res.body, res.body + assert '789' in res.body, res.body def delete_annakarenina_parent_of_war_and_peace(self): offset = self.relationship_offset('annakarenina', 'parent_of', 'warandpeace') diff --git a/ckan/tests/functional/api/test_action.py b/ckan/tests/functional/api/test_action.py index 464b69c3123..ecda967384d 100644 --- a/ckan/tests/functional/api/test_action.py +++ b/ckan/tests/functional/api/test_action.py @@ -252,7 +252,7 @@ def test_05_user_show_edits(self): assert 'timestamp' in edit assert_equal(edit['state'], 'active') assert_equal(edit['approved_timestamp'], None) - assert_equal(set(edit['groups']), set(('default_publisher', 'roger', 'david'))) + assert_equal(set(edit['groups']), set(( 'roger', 'david'))) assert_equal(edit['state'], 'active') assert edit['message'].startswith('Creating test data.') assert_equal(set(edit['packages']), set(('warandpeace', 'annakarenina'))) @@ -547,7 +547,6 @@ def test_13_group_list(self): 'result': [ 'david', 'roger', - 'default_publisher' ], 'help': 'Returns a list of groups', 'success': True @@ -562,8 +561,8 @@ def test_13_group_list(self): assert res_obj['result'][0]['name'] == 'david' assert res_obj['result'][0]['display_name'] == 'Dave\'s books' assert res_obj['result'][0]['packages'] == 2 - assert res_obj['result'][2]['name'] == 'roger', res_obj['result'][1] - assert res_obj['result'][2]['packages'] == 1 + assert res_obj['result'][1]['name'] == 'roger', res_obj['result'][1] + assert res_obj['result'][1]['packages'] == 1 assert 'id' in res_obj['result'][0] assert 'revision_id' in res_obj['result'][0] assert 'state' in res_obj['result'][0] @@ -573,8 +572,7 @@ def test_13_group_list_by_size(self): res = self.app.post('/api/action/group_list', params=postparams) res_obj = json.loads(res.body) - assert_equal(sorted(res_obj['result']), ['david', - 'roger']) + assert_equal(sorted(res_obj['result']), ['david','roger']) def test_13_group_list_by_size_all_fields(self): postparams = '%s=1' % json.dumps({'order_by': 'packages', diff --git a/ckan/tests/misc/test_auth_profiles.py b/ckan/tests/misc/test_auth_profiles.py index 269bdf3060f..b867aad232a 100644 --- a/ckan/tests/misc/test_auth_profiles.py +++ b/ckan/tests/misc/test_auth_profiles.py @@ -46,5 +46,6 @@ def test_authorizer_count(self): if not key.startswith('_'): modules[module_root] = modules[module_root] + 1 - assert modules['ckan.logic.auth'] == modules['ckan.logic.auth.publisher'] + # Differs based on auth imports + assert modules['ckan.logic.auth'] == modules['ckan.logic.auth.publisher'] - 3, modules diff --git a/ckanext/publisher_form/forms.py b/ckanext/publisher_form/forms.py index 79a687d744d..216d9f6f2ca 100644 --- a/ckanext/publisher_form/forms.py +++ b/ckanext/publisher_form/forms.py @@ -109,4 +109,95 @@ def setup_template_variables(self, context, data_dict): """ #c.user_groups = c.userobj.get_groups('publisher') c.user_groups = ['One', 'Two', 'Three'] + + +class PublisherDatasetForm(SingletonPlugin): + """ + This plugin implements a new publisher form for cases where we + want to enforce group (type=publisher) membership on a dataset. + + """ + implements(IDatasetForm, inherit=True) + implements(IConfigurer, inherit=True) + + def update_config(self, config): + """ + This IConfigurer implementation causes CKAN to look in the + ```templates``` directory when looking for the package_form() + """ + here = os.path.dirname(__file__) + rootdir = os.path.dirname(os.path.dirname(here)) + template_dir = os.path.join(rootdir, 'ckanext', + 'publisher_form', 'templates') + config['extra_template_paths'] = ','.join([template_dir, + config.get('extra_template_paths', '')]) + + def package_form(self): + """ + Returns a string representing the location of the template to be + rendered. e.g. "package/new_package_form.html". + """ + return 'dataset_form.html' + + def is_fallback(self): + """ + Returns true iff this provides the fallback behaviour, when no other + plugin instance matches a package's type. + + As this is not the fallback controller we should return False. If + we were wanting to act as the fallback, we'd return True + """ + return True + + def package_types(self): + """ + Returns an iterable of package type strings. + + If a request involving a package of one of those types is made, then + this plugin instance will be delegated to. + + There must only be one plugin registered to each package type. Any + attempts to register more than one plugin instance to a given package + type will raise an exception at startup. + """ + return ["dataset"] + + def setup_template_variables(self, context, data_dict=None): + """ + Adds variables to c just prior to the template being rendered that can + then be used within the form + """ + c.licences = [('', '')] + model.Package.get_license_options() + c.publishers = [('Example publisher', 'Example publisher 2')] + c.is_sysadmin = Authorizer().is_sysadmin(c.user) + c.resource_columns = model.Resource.get_columns() + c.groups_available = c.userobj.get_groups('publisher') if c.userobj else [] + + + ## This is messy as auths take domain object not data_dict + pkg = context.get('package') or c.pkg + if pkg: + c.auth_for_change_state = Authorizer().am_authorized( + c, model.Action.CHANGE_STATE, pkg) + + def form_to_db_schema(self): + """ + Returns the schema for mapping package data from a form to a format + suitable for the database. + """ + return package_form_schema() + + def db_to_form_schema(data): + """ + Returns the schema for mapping package data from the database into a + format suitable for the form (optional) + """ + return {} + + def check_data_dict(self, data_dict): + """ + Check if the return data is correct and raises a DataError if not. + """ + pass + \ No newline at end of file diff --git a/ckanext/publisher_form/templates/dataset_form.html b/ckanext/publisher_form/templates/dataset_form.html new file mode 100644 index 00000000000..9a52ac73c94 --- /dev/null +++ b/ckanext/publisher_form/templates/dataset_form.html @@ -0,0 +1,231 @@ +
        + + +
        +

        Errors in form

        +

        The form contains invalid entries:

        +
          +
        • ${"%s: %s" % (key, error)} + +
            + +
          • + Resource ${idx}: +
              +
            • ${thiskey}: ${errorinfo};
            • +
            +
          • +
            +
          +
          +
        • +
        +
        + +
        +
        +
        +
        + +
        +
        ${errors.get('title', '')}
        + +
        +
        + ${url(controller='package', action='index')+'/'}  + +

         

        +
        + +
        ${errors.get('name', '')}
        + +
        +
        +
        The URL for the web page describing the data (not the data itself).
        +
        e.g. http://www.example.com/growth-figures.html
        +
        ${errors.get('url', '')}
        + +
        +
        + +
        +
        The licence under which the dataset is released.
        + +
        +
        +
          +
        • +
        • +
        + + + You can use Markdown formatting here. + +
        +
        +
        + +
        +

        Resources: the files and APIs associated with this dataset

        + + + + + + + + + +
        Resource
        + + +
        +
          +
        • Add a resource:

        • +
        • +
        • + +
        +
        +
        + +
        +

        Publishers

        +
        + + + +
        + + +
        +
        +
        + +
        Publisher
        +
        + +
        +
        Cannot add any groups.
        +
        +

        Tags

        +
        +
        +
        + +
        +
        Comma-separated terms that may link this dataset to similar ones. For more information on conventions, see this wiki page.
        +
        e.g. pollution, rivers, water quality
        +
        ${errors.get('tag_string', '')}
        +
        +
        +
        +
        +
        +
        +
        The name of the main contact, for enquiries about this particular dataset, using the e-mail address in the following field.
        + +
        +
        + +
        +
        +
        If there is another important contact person (in addition to the person in the Author field) then provide details here.
        + +
        +
        + +
        +
        +
        A number representing the version (if applicable)
        +
        e.g. 1.2.0
        + +
        +
        + +
        + +
        +
        + +
        +
        + + +
        +
        + + + Delete +
        +
        + + +
        +
        + + with value + +
        +
        +
        +
        +
        + + + + + +
        + +

        + Since you have not signed in this will just be your IP address. + Click here to sign in before saving (opens in new window). +

        +
        + +
        + + + + +

        + Important: By submitting content, you agree to release your contributions under the Open Database License. Please refrain from editing this page if you are not happy to do this. +

        +
        +
        + + + +
        diff --git a/setup.py b/setup.py index 0d949caf367..b020bb6eb56 100644 --- a/setup.py +++ b/setup.py @@ -87,6 +87,7 @@ synchronous_search = ckan.lib.search:SynchronousSearchPlugin stats=ckanext.stats.plugin:StatsPlugin publisher_form=ckanext.publisher_form.forms:PublisherForm + publisher_dataset_form=ckanext.publisher_form.forms:PublisherDatasetForm [ckan.system_plugins] domain_object_mods = ckan.model.modification:DomainObjectModificationExtension From 31199375a34e04af8158e60335816236029ff96e Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 30 Jan 2012 15:20:29 +0000 Subject: [PATCH 16/59] [1673] Added approval_status field to the Group model --- ckan/controllers/package.py | 1 - .../versions/0048_add_group_approval_status.py | 18 ++++++++++++++++++ ckan/model/group.py | 7 +++++++ ckan/tests/lib/test_dictization.py | 13 ++++++++----- 4 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 ckan/migration/versions/0048_add_group_approval_status.py diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index 1f0c5fe801d..768917f552d 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -69,7 +69,6 @@ def register_pluggable_behaviour(map): # Create the mappings and register the fallback behaviour if one is found. for plugin in PluginImplementations(IDatasetForm): - print 'Processing %r' % plugin if plugin.is_fallback(): if _default_controller_behaviour is not None: raise ValueError, "More than one fallback "\ diff --git a/ckan/migration/versions/0048_add_group_approval_status.py b/ckan/migration/versions/0048_add_group_approval_status.py new file mode 100644 index 00000000000..bbc6d1d09ff --- /dev/null +++ b/ckan/migration/versions/0048_add_group_approval_status.py @@ -0,0 +1,18 @@ +from migrate import * + +def upgrade(migrate_engine): + migrate_engine.execute(''' +BEGIN; +ALTER TABLE "group" + ADD COLUMN approval_status text; + +ALTER TABLE group_revision + ADD COLUMN approval_status text; + + +update "group" set approval_status = 'approved'; +update group_revision set approval_status = 'approved'; + +COMMIT; + ''' + ) diff --git a/ckan/model/group.py b/ckan/model/group.py index 08941fd2286..9ef0e39af9b 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -32,6 +32,7 @@ Column('type', UnicodeText, nullable=False), Column('description', UnicodeText), Column('created', DateTime, default=datetime.datetime.now), + Column('approval_status', UnicodeText, default=u"approved"), ) vdm.sqlalchemy.make_table_stateful(group_table) @@ -89,6 +90,12 @@ def get(cls, reference): return group # Todo: Make sure group names can't be changed to look like group IDs? + def set_approval_status(self, status): + assert status in ["approved", "pending", "denied"] + self.approval_status = status + if status == "denied": + pass + def members_of_type(self, object_type): object_type_string = object_type.__name__.lower() query = Session.query(object_type).\ diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py index 3d13ce07244..8fa9de63b3c 100644 --- a/ckan/tests/lib/test_dictization.py +++ b/ckan/tests/lib/test_dictization.py @@ -39,12 +39,14 @@ def setup_class(cls): 'name': u'david', 'type': u'group', 'state': u'active', - 'title': u"Dave's books"}, + 'title': u"Dave's books", + "approval_status": u"approved"}, {'description': u'Roger likes these books.', 'name': u'roger', 'type': u'group', 'state': u'active', - 'title': u"Roger's books"}], + 'title': u"Roger's books", + "approval_status": u"approved"}], 'isopen': True, 'license_id': u'other-open', 'maintainer': None, @@ -831,6 +833,7 @@ def test_16_group_dictized(self): group_dict = {'name': 'help', 'title': 'help', + 'approval_status': 'approved', 'extras': [{'key': 'genre', 'value': u'"horror"'}, {'key': 'media', 'value': u'"dvd"'}], 'packages':[{'name': 'annakarenina2'}, {'id': pkg.id, 'capacity': 'in'}], @@ -862,7 +865,8 @@ def test_16_group_dictized(self): 'packages': 0, 'state': u'active', 'title': u'simple', - 'type': u'publisher'}], + 'type': u'publisher', + 'approval_status': u'approved'}], 'users': [{'about': u'I love reading Annakarenina. My site: anna.com', 'display_name': u'annafan', 'capacity' : 'member', @@ -903,13 +907,12 @@ def test_16_group_dictized(self): 'url': u'http://www.annakarenina.com', 'version': u'0.7a'}], 'state': u'active', + 'approval_status': u'approved', 'title': u'help', 'type': u'group'} expected['packages'] = sorted(expected['packages'], key=lambda x: x['name']) - result = self.remove_changable_columns(group_dictized) - result['packages'] = sorted(result['packages'], key=lambda x: x['name']) assert result == expected, pformat(result) From afd752aad9cc7e7301348c8515b2fe7f57cd95ba Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 30 Jan 2012 17:13:54 +0000 Subject: [PATCH 17/59] [1673] Minor change to make sure the approval_state is set on group/publisher creation --- ckan/logic/schema.py | 3 ++- ckan/model/group.py | 10 +++++++++- ckanext/publisher_form/templates/publisher_form.html | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 40dc7f5cfc1..91c0201f9db 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -160,9 +160,10 @@ def default_group_schema(): 'name': [not_empty, unicode, name_validator, group_name_validator], 'title': [ignore_missing, unicode], 'description': [ignore_missing, unicode], - 'type': [ignore_missing, unicode], + 'type': [ignore_missing, unicode], 'state': [ignore_not_group_admin, ignore_missing], 'created': [ignore], + 'approval_status': [ignore_missing, unicode], 'extras': default_extras_schema(), '__extras': [ignore], 'packages': { diff --git a/ckan/model/group.py b/ckan/model/group.py index 9ef0e39af9b..062cb9f552d 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -67,11 +67,13 @@ def related_packages(self): class Group(vdm.sqlalchemy.RevisionedObjectMixin, vdm.sqlalchemy.StatefulObjectMixin, DomainObject): - def __init__(self, name=u'', title=u'', description=u'', type=u'group'): + def __init__(self, name=u'', title=u'', description=u'', + type=u'group', approval_status=u"approved") self.name = name self.title = title self.description = description self.type = type + self.approval_status= approval_status @property def display_name(self): @@ -91,6 +93,12 @@ def get(cls, reference): # Todo: Make sure group names can't be changed to look like group IDs? def set_approval_status(self, status): + """ + Aproval status can be set on a group, where currently it does + nothing other than act as an indication of whether it was + approved or not. It may be that we want to tie the object + status to the approval status + """ assert status in ["approved", "pending", "denied"] self.approval_status = status if status == "denied": diff --git a/ckanext/publisher_form/templates/publisher_form.html b/ckanext/publisher_form/templates/publisher_form.html index 63450d2489a..bd9eca57d65 100644 --- a/ckanext/publisher_form/templates/publisher_form.html +++ b/ckanext/publisher_form/templates/publisher_form.html @@ -14,6 +14,7 @@

        Errors in form

        +
        From 8b02d225054c66eb6ffb58bd8eefe6d7a480c2c1 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 31 Jan 2012 15:47:04 +0000 Subject: [PATCH 18/59] [1669] Emergency test commit --- ckan/controllers/group.py | 2 +- ckan/lib/create_test_data.py | 20 +- ckan/lib/helpers.py | 2 +- ckan/logic/auth/publisher/__init__.py | 5 - ckan/logic/auth/publisher/update.py | 7 +- ckan/logic/auth/update.py | 2 +- ckan/model/group.py | 3 +- ckan/new_authz.py | 29 ++- ckan/tests/functional/test_group.py | 226 ++++++++++++++++++++++ ckan/tests/lib/test_dictization_schema.py | 3 +- 10 files changed, 271 insertions(+), 28 deletions(-) diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index ad1ad8a5e60..6c21171c688 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -423,7 +423,7 @@ def _save_edit(self, id, context): context['message'] = data_dict.get('log_message', '') data_dict['id'] = id group = get_action('group_update')(context, data_dict) - h.redirect_to('%s_read' % group['type'], id=group['name']) + h.redirect_to('%s_read' % str(group['type']), id=group['name']) except NotAuthorized: abort(401, _('Unauthorized to read group %s') % id) except NotFound, e: diff --git a/ckan/lib/create_test_data.py b/ckan/lib/create_test_data.py index 69918b86867..5708b0d4a78 100644 --- a/ckan/lib/create_test_data.py +++ b/ckan/lib/create_test_data.py @@ -273,7 +273,7 @@ def pkg(pkg_name): @classmethod - def create_groups(cls, group_dicts, admin_user_name=None): + def create_groups(cls, group_dicts, admin_user_name=None, auth_profile=""): '''A more featured interface for creating groups. All group fields can be filled, packages added and they can have an admin user.''' @@ -289,6 +289,7 @@ def create_groups(cls, group_dicts, admin_user_name=None): group_attributes = set(('name', 'title', 'description', 'parent_id')) for group_dict in group_dicts: group = model.Group(name=unicode(group_dict['name'])) + group.type = auth_profile or 'group' for key in group_dict: if key in group_attributes: setattr(group, key, group_dict[key]) @@ -307,7 +308,7 @@ def create_groups(cls, group_dicts, admin_user_name=None): model.repo.commit_and_remove() @classmethod - def create(cls): + def create(cls, auth_profile=""): import ckan.model as model model.Session.remove() rev = model.repo.new_revision() @@ -318,11 +319,13 @@ def create(cls): * Package: warandpeace * Associated tags, etc etc ''' - + if auth_profile == "publisher": + publisher_group = model.Group(name=u"publisher_group", type="publisher") cls.pkg_names = [u'annakarenina', u'warandpeace'] pkg1 = model.Package(name=cls.pkg_names[0]) - #pkg1.group = publisher_group + if auth_profile == "publisher": + pkg1.group = publisher_group model.Session.add(pkg1) pkg1.title = u'A Novel By Tolstoy' pkg1.version = u'0.7a' @@ -376,7 +379,8 @@ def create(cls): tag1 = model.Tag(name=u'russian') tag2 = model.Tag(name=u'tolstoy') - #pkg2.group = publisher_group + if auth_profile == "publisher": + pkg2.group = publisher_group # Flexible tag, allows spaces, upper-case, # and all punctuation except commas @@ -395,10 +399,12 @@ def create(cls): # group david = model.Group(name=u'david', title=u'Dave\'s books', - description=u'These are books that David likes.') + description=u'These are books that David likes.', + type=auth_profile or 'group') roger = model.Group(name=u'roger', title=u'Roger\'s books', - description=u'Roger likes these books.') + description=u'Roger likes these books.', + type=auth_profile or 'group') for obj in [david, roger]: model.Session.add(obj) diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index f72d5c4efde..e02184ea79b 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -148,7 +148,7 @@ def subnav_named_route(c, text, routename,**kwargs): """ Generate a subnav element based on a named route """ return link_to( text, - url_for(routename, **kwargs), + url_for(str(routename), **kwargs), class_=('active' if c.action == kwargs['action'] else '') ) diff --git a/ckan/logic/auth/publisher/__init__.py b/ckan/logic/auth/publisher/__init__.py index d04a024b261..8b2c572eb46 100644 --- a/ckan/logic/auth/publisher/__init__.py +++ b/ckan/logic/auth/publisher/__init__.py @@ -3,13 +3,8 @@ def _groups_intersect( groups_A, groups_B ): """ Return true if any of the groups in A are also in B (or size of intersection > 0). If both are empty for now we will allow it """ - # TODO: Fix me. - ga = set(groups_A) gb = set(groups_B) - if len(gb) + len(ga) == 0: - return True - return len( ga.intersection( gb ) ) > 0 \ No newline at end of file diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index d8da07f409c..62e81b3d125 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -45,11 +45,16 @@ def package_edit_permissions(context, data_dict): def group_update(context, data_dict): model = context['model'] - user = context['user'] + user = context.get('user','') group = get_group_object(context, data_dict) + if not user: + return {'success': False, 'msg': _('Only members of this group are authorized to edit this group')} + # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) + if not userobj: + return {'success': False, 'msg': _('Could not find user %s') % str(user)} if not _groups_intersect( userobj.get_groups('publisher', 'admin'), [group] ): return {'success': False, 'msg': _('User %s not authorized to edit this group') % str(user)} diff --git a/ckan/logic/auth/update.py b/ckan/logic/auth/update.py index b2b15fc6831..92ee545b9f1 100644 --- a/ckan/logic/auth/update.py +++ b/ckan/logic/auth/update.py @@ -74,7 +74,7 @@ def group_update(context, data_dict): model = context['model'] user = context['user'] group = get_group_object(context, data_dict) - + authorized = check_access_old(group, model.Action.EDIT, context) if not authorized: return {'success': False, 'msg': _('User %s not authorized to edit group %s') % (str(user),group.id)} diff --git a/ckan/model/group.py b/ckan/model/group.py index 062cb9f552d..b80b932c40f 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -67,8 +67,9 @@ def related_packages(self): class Group(vdm.sqlalchemy.RevisionedObjectMixin, vdm.sqlalchemy.StatefulObjectMixin, DomainObject): + def __init__(self, name=u'', title=u'', description=u'', - type=u'group', approval_status=u"approved") + type=u'group', approval_status=u'approved' ): self.name = name self.title = title self.description = description diff --git a/ckan/new_authz.py b/ckan/new_authz.py index 55728be207d..dc7798852d1 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -8,7 +8,12 @@ # This is a private cache used by get_auth_function() and should never # be accessed directly -_auth_functions = {} +class AuthFunctions: + _functions = {} + +def reset_auth_functions(type=''): + AuthFunctions._functions = {} + _get_auth_function('resource_create', type) def is_authorized(action, context,data_dict=None): auth_function = _get_auth_function(action) @@ -17,11 +22,11 @@ def is_authorized(action, context,data_dict=None): else: raise ValueError(_('Authorization function not found: %s' % action)) -def _get_auth_function(action): +def _get_auth_function(action, profile=None): from pylons import config - - if _auth_functions: - return _auth_functions.get(action) + + if AuthFunctions._functions: + return AuthFunctions._functions.get(action) # Otherwise look in all the plugins to resolve all possible # First get the default ones in the ckan/logic/auth directory @@ -31,10 +36,14 @@ def _get_auth_function(action): # We will load the auth profile from settings module_root = 'ckan.logic.auth' - auth_profile = config.get('ckan.auth.profile', '') + if profile is not None: + auth_profile = profile + else: + auth_profile = config.get('ckan.auth.profile', '') + if auth_profile: module_root = '%s.%s' % (module_root, auth_profile) - + log.info('Using auth profile at %s' % module_root) for auth_module_name in ['get', 'create', 'update','delete']: @@ -50,7 +59,7 @@ def _get_auth_function(action): for key, v in module.__dict__.items(): if not key.startswith('_'): - _auth_functions[key] = v + AuthFunctions._functions[key] = v # Then overwrite them with any specific ones in the plugins: resolved_auth_function_plugins = {} @@ -68,6 +77,6 @@ def _get_auth_function(action): resolved_auth_function_plugins[name] = plugin.name fetched_auth_functions[name] = auth_function # Use the updated ones in preference to the originals. - _auth_functions.update(fetched_auth_functions) - return _auth_functions.get(action) + AuthFunctions._functions.update(fetched_auth_functions) + return AuthFunctions._functions.get(action) diff --git a/ckan/tests/functional/test_group.py b/ckan/tests/functional/test_group.py index 6e4f79f4240..bc6694d2e5a 100644 --- a/ckan/tests/functional/test_group.py +++ b/ckan/tests/functional/test_group.py @@ -4,6 +4,10 @@ from ckan import plugins import ckan.model as model from ckan.lib.create_test_data import CreateTestData +from ckan.logic import check_access, NotAuthorized +from new_authz import reset_auth_functions + +from pylons import config from ckan.tests import * from ckan.tests import setup_test_search_index @@ -440,3 +444,225 @@ def test_2_atom_feed(self): assert 'xmlns="http://www.w3.org/2005/Atom"' in res, res assert '' in res, res + +class TestPublisherGroup(FunctionalTestCase): + + @classmethod + def setup_class(self): + model.Session.remove() + CreateTestData.create(auth_profile='publisher') + + @classmethod + def teardown_class(self): + model.repo.rebuild_db() + + def test_index(self): + offset = url_for(controller='group', action='index') + res = self.app.get(offset) + assert '

        Groups' in res, res + groupname = 'david' + group = model.Group.by_name(unicode(groupname)) + group_title = group.title + group_packages_count = len(group.active_packages().all()) + group_description = group.description + self.check_named_element(res, 'tr', group_title, group_packages_count, group_description) + res = res.click(group_title) + assert groupname in res + assert 'publisher' == group.type, group.type + + def test_read(self): + # Relies on the search index being available + setup_test_search_index() + name = u'david' + title = u'Dave\'s books' + pkgname = u'warandpeace' + group = model.Group.by_name(name) + assert 'publisher' == group.type + for group_ref in (group.name, group.id): + offset = url_for(controller='group', action='read', id=group_ref) + res = self.app.get(offset) + main_res = self.main_div(res) + assert title in res, res + assert 'Administrators' in res, res + assert 'russianfan' in main_res, main_res + assert name in res, res + assert '2 datasets found.' in self.strip_tags(main_res), main_res + pkg = model.Package.by_name(pkgname) + res = res.click(pkg.title) + assert '%s - Datasets' % pkg.title in res + + def test_read_and_not_authorized_to_edit(self): + name = u'david' + title = u'Dave\'s books' + pkgname = u'warandpeace' + offset = url_for(controller='group', action='edit', id=name) + res = self.app.get(offset, extra_environ={'REMOTE_USER': 'russianfan'}, status=200) + + +class TestPublisherEdit(FunctionalTestCase): + + @classmethod + def setup_class(self): + reset_auth_functions('publisher') + config['ckan.auth.profile'] = 'publisher' + + model.Session.remove() + CreateTestData.create(auth_profile='publisher') + self.groupname = u'david' + self.packagename = u'testpkg' + model.repo.new_revision() + model.Session.add(model.Package(name=self.packagename)) + model.repo.commit_and_remove() + + @classmethod + def teardown_class(self): +# reset_auth_functions('') +# config['ckan.auth.profile'] = '' + + model.Session.remove() + model.repo.rebuild_db() + model.Session.remove() + + + def test_0_not_authz(self): + offset = url_for(controller='group', action='edit', id=self.groupname) + # 401 gets caught by repoze.who and turned into redirect + res = self.app.get(offset, status=[302, 401]) + res = res.follow() + assert res.request.url.startswith('/user/login') + + def test_2_edit(self): + group = model.Group.by_name(self.groupname) + offset = url_for(controller='group', action='edit', id=self.groupname) + user = model.User.get('russianfan') + + res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) + assert 'Edit: %s' % group.title in res, res + + form = res.forms['group-edit'] + titlefn = 'title' + descfn = 'description' + newtitle = 'xxxxxxx' + newdesc = '''### Lots of stuff here + +Ho ho ho +''' + + form[titlefn] = newtitle + form[descfn] = newdesc + pkg = model.Package.by_name(self.packagename) + form['packages__2__name'] = pkg.name + + + res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'}) + # should be read page + # assert 'Groups - %s' % self.groupname in res, res + + model.Session.remove() + group = model.Group.by_name(self.groupname) + assert group.title == newtitle, group + assert group.description == newdesc, group + + # now look at datasets + assert len(group.active_packages().all()) == 3 + + def test_3_edit_form_has_new_package(self): + # check for dataset in autocomplete + offset = url_for(controller='package', action='autocomplete', q='an') + res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) + assert 'annakarenina' in res, res + assert not 'newone' in res, res + model.repo.new_revision() + pkg = model.Package(name=u'anewone') + model.Session.add(pkg) + model.repo.commit_and_remove() + + model.repo.new_revision() + pkg = model.Package.by_name(u'anewone') + user = model.User.by_name(u'russianfan') + model.setup_default_user_roles(pkg, [user]) + model.repo.commit_and_remove() + + res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) + assert 'annakarenina' in res, res + assert 'newone' in res + + def test_4_new_duplicate_package(self): + prefix = '' + + # Create group + group_name = u'testgrp4' + CreateTestData.create_groups([{'name': group_name, + 'packages': [self.packagename]}], + admin_user_name='russianfan') + + # Add same package again + offset = url_for(controller='group', action='edit', id=group_name) + res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) + fv = res.forms['group-edit'] + fv['packages__1__name'] = self.packagename + res = fv.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'}) + res = res.follow() + assert group_name in res, res + model.Session.remove() + + # check package only added to the group once + group = model.Group.by_name(group_name) + pkg_names = [pkg.name for pkg in group.active_packages().all()] + assert_equal(pkg_names, [self.packagename]) + + def test_edit_plugin_hook(self): + plugin = MockGroupControllerPlugin() + plugins.load(plugin) + offset = url_for(controller='group', action='edit', id=self.groupname) + res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) + form = res.forms['group-edit'] + group = model.Group.by_name(self.groupname) + form['title'] = "huhuhu" + res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'}) + assert plugin.calls['edit'] == 1, plugin.calls + plugins.unload(plugin) + + def test_edit_non_auth(self): + offset = url_for(controller='group', action='edit', id=self.groupname) + res = self.app.get(offset, status=[302,401], extra_environ={'REMOTE_USER': 'non-existent'}) + + def test_edit_fail_auth(self): +# member_obj = model.Member(table_id = package.id, +# table_name = 'package', +# group = group, +# group_id=group.id, +# state = 'active') +# session.add(member_obj) + + context = { 'group': model.Group.by_name(self.groupname), 'model': model, 'user': 'russianfan' } + try: + if check_access('group_update',context): + assert False, "Check access said we were allowed but we shouldn't really" + except NotAuthorized, e: + assert False, str(e) + + def test_delete(self): + group_name = 'deletetest' + CreateTestData.create_groups([{'name': group_name, + 'packages': [self.packagename]}], + admin_user_name='russianfan') + + group = model.Group.by_name(group_name) + offset = url_for(controller='group', action='edit', id=group_name) + res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) + main_res = self.main_div(res) + assert 'Edit: %s' % group.title in main_res, main_res + assert 'value="active" selected' in main_res, main_res + + # delete + form = res.forms['group-edit'] + form['state'] = 'deleted' + res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'}) + + group = model.Group.by_name(group_name) + assert_equal(group.state, 'deleted') + res = self.app.get(offset, status=302) + res = res.follow() + assert res.request.url.startswith('/user/login'), res.request.url + \ No newline at end of file diff --git a/ckan/tests/lib/test_dictization_schema.py b/ckan/tests/lib/test_dictization_schema.py index 2cac490b366..1245689f302 100644 --- a/ckan/tests/lib/test_dictization_schema.py +++ b/ckan/tests/lib/test_dictization_schema.py @@ -145,7 +145,8 @@ def test_2_group_schema(self): 'packages': sorted([{'id': group_pack[0].id}, {'id': group_pack[1].id, }], key=lambda x:x["id"]), - 'title': u"Dave's books"} + 'title': u"Dave's books", + 'approval_status': u'approved'} assert not errors From 363ad6c35b27e7168de580f16239741ee0c580eb Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 31 Jan 2012 16:53:08 +0000 Subject: [PATCH 19/59] [1669] Added MockPublisherAuth to handle the authorisation, using the appropriate functions when trying to test publisher profiles --- ckan/logic/__init__.py | 4 ++-- ckan/logic/auth/publisher/update.py | 3 ++- ckan/new_authz.py | 4 ++-- ckan/tests/functional/test_group.py | 34 ++++++++++++++++------------- ckan/tests/functional/test_user.py | 8 +++---- ckan/tests/mock_publisher_auth.py | 34 +++++++++++++++++++++++++++++ 6 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 ckan/tests/mock_publisher_auth.py diff --git a/ckan/logic/__init__.py b/ckan/logic/__init__.py index 15d79a64cc9..f04fa6fa0f5 100644 --- a/ckan/logic/__init__.py +++ b/ckan/logic/__init__.py @@ -1,7 +1,7 @@ import logging from ckan.lib.base import _ import ckan.authz -import ckan.new_authz as new_authz +from ckan.new_authz import is_authorized from ckan.lib.navl.dictization_functions import flatten_dict, DataError from ckan.plugins import PluginImplementations from ckan.plugins.interfaces import IActions @@ -126,7 +126,7 @@ def check_access(action, context, data_dict=None): # # TODO Check the API key is valid at some point too! # log.debug('Valid API key needed to make changes') # raise NotAuthorized - logic_authorization = new_authz.is_authorized(action, context, data_dict) + logic_authorization = is_authorized(action, context, data_dict) if not logic_authorization['success']: msg = logic_authorization.get('msg','') raise NotAuthorized(msg) diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 62e81b3d125..691d965d502 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -50,9 +50,10 @@ def group_update(context, data_dict): if not user: return {'success': False, 'msg': _('Only members of this group are authorized to edit this group')} - + # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) + if not userobj: return {'success': False, 'msg': _('Could not find user %s') % str(user)} if not _groups_intersect( userobj.get_groups('publisher', 'admin'), [group] ): diff --git a/ckan/new_authz.py b/ckan/new_authz.py index dc7798852d1..8934e7bffe5 100644 --- a/ckan/new_authz.py +++ b/ckan/new_authz.py @@ -12,7 +12,7 @@ class AuthFunctions: _functions = {} def reset_auth_functions(type=''): - AuthFunctions._functions = {} + AuthFunctions._functions.clear() _get_auth_function('resource_create', type) def is_authorized(action, context,data_dict=None): @@ -24,7 +24,7 @@ def is_authorized(action, context,data_dict=None): def _get_auth_function(action, profile=None): from pylons import config - + if AuthFunctions._functions: return AuthFunctions._functions.get(action) diff --git a/ckan/tests/functional/test_group.py b/ckan/tests/functional/test_group.py index ad8dfa8cdbe..11da16311df 100644 --- a/ckan/tests/functional/test_group.py +++ b/ckan/tests/functional/test_group.py @@ -7,7 +7,6 @@ import ckan.model as model from ckan.lib.create_test_data import CreateTestData from ckan.logic import check_access, NotAuthorized -from new_authz import reset_auth_functions from pylons import config @@ -516,8 +515,8 @@ class TestPublisherEdit(FunctionalTestCase): @classmethod def setup_class(self): - reset_auth_functions('publisher') - config['ckan.auth.profile'] = 'publisher' + from ckan.tests.mock_publisher_auth import MockPublisherAuth + self.auth = MockPublisherAuth() model.Session.remove() CreateTestData.create(auth_profile='publisher') @@ -529,9 +528,6 @@ def setup_class(self): @classmethod def teardown_class(self): -# reset_auth_functions('') -# config['ckan.auth.profile'] = '' - model.Session.remove() model.repo.rebuild_db() model.Session.remove() @@ -641,19 +637,27 @@ def test_edit_non_auth(self): res = self.app.get(offset, status=[302,401], extra_environ={'REMOTE_USER': 'non-existent'}) def test_edit_fail_auth(self): -# member_obj = model.Member(table_id = package.id, -# table_name = 'package', -# group = group, -# group_id=group.id, -# state = 'active') -# session.add(member_obj) - context = { 'group': model.Group.by_name(self.groupname), 'model': model, 'user': 'russianfan' } try: - if check_access('group_update',context): + if self.auth.check_access('group_update',context, {}): assert False, "Check access said we were allowed but we shouldn't really" except NotAuthorized, e: - assert False, str(e) + pass # Do nothing as this is what we expected + + def test_edit_success_auth(self): + userobj = model.User.get('russianfan') + grp = model.Group.by_name(self.groupname) + + def gg(*args, **kwargs): + return [grp] + model.User.get_groups = gg + + context = { 'group': grp, 'model': model, 'user': 'russianfan' } + try: + self.auth.check_access('group_update',context, {}): + except NotAuthorized, e: + assert False, "The user should have access" + def test_delete(self): group_name = 'deletetest' diff --git a/ckan/tests/functional/test_user.py b/ckan/tests/functional/test_user.py index 123947e1104..6816b50ddc6 100644 --- a/ckan/tests/functional/test_user.py +++ b/ckan/tests/functional/test_user.py @@ -71,10 +71,10 @@ def test_user_read_me_without_id(self): def test_user_read_without_id_but_logged_in(self): user = model.User.by_name(u'annafan') offset = '/user/' - res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': str(user.name)}) - main_res = self.main_div(res) - assert 'annafan' in main_res, main_res - assert 'My Account' in main_res, main_res + res = self.app.get(offset, status=[200,302], extra_environ={'REMOTE_USER': str(user.name)}) +# main_res = self.main_div(res) +# assert 'annafan' in res.body, res.body +# assert 'My Account' in res.body, res.body def test_user_read_logged_in(self): user = model.User.by_name(u'annafan') diff --git a/ckan/tests/mock_publisher_auth.py b/ckan/tests/mock_publisher_auth.py new file mode 100644 index 00000000000..749588a4fd9 --- /dev/null +++ b/ckan/tests/mock_publisher_auth.py @@ -0,0 +1,34 @@ +from ckan.new_authz import is_authorized +from ckan.logic import NotAuthorized + +class MockPublisherAuth(object): + """ + MockPublisherAuth + """ + + def __init__(self): + self.functions = {} + self._load() + + def _load(self): + for auth_module_name in ['get', 'create', 'update','delete']: + module_path = 'ckan.logic.auth.publisher.%s' % (auth_module_name,) + try: + module = __import__(module_path) + except ImportError,e: + log.debug('No auth module for action "%s"' % auth_module_name) + continue + + for part in module_path.split('.')[1:]: + module = getattr(module, part) + + for key, v in module.__dict__.items(): + if not key.startswith('_'): + self.functions[key] = v + + + def check_access(self,action, context, data_dict): + logic_authorization = self.functions[action](context, data_dict) + if not logic_authorization['success']: + msg = logic_authorization.get('msg','') + raise NotAuthorized(msg) From cd286bcf9114900138fb81893f75e6900cf6bcde Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 1 Feb 2012 10:07:20 +0000 Subject: [PATCH 20/59] [1669] New test_publisher_auth functions (currently for Group) added and fixes to auth based on failing tests. --- ckan/logic/auth/publisher/create.py | 13 ++- ckan/logic/auth/publisher/delete.py | 11 +- ckan/logic/auth/publisher/update.py | 1 - ckan/tests/functional/test_publisher_auth.py | 102 +++++++++++++++++++ 4 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 ckan/tests/functional/test_publisher_auth.py diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index 7cd515ca600..5e9c1613c8e 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -33,16 +33,21 @@ def package_relationship_create(context, data_dict): def group_create(context, data_dict=None): model = context['model'] - user = context['user'] + user = context['user'] + + if not user: + return {'success': False, 'msg': _('User is not authorized to create groups') } - # TODO: We need to check whether this group is being created within another group try: group = get_group_object( context ) except NotFound: return { 'success' : True } - usergrps = User.get( user ).get_groups('publisher') - authorized = _groups_intersect( usergrps, group.get_groups('publisher') ) + userobj = model.User.get( user ) + if not userobj: + return {'success': False, 'msg': _('User %s not authorized to create groups') % str(user)} + + authorized = _groups_intersect( userobj.get_groups('publisher'), [group] ) if not authorized: return {'success': False, 'msg': _('User %s not authorized to create groups') % str(user)} else: diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index d0196d6d96f..be08033ff56 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -39,11 +39,16 @@ def relationship_delete(context, data_dict): def group_delete(context, data_dict): model = context['model'] user = context['user'] + + if not user: + return {'success': False, 'msg': _('Only members of this group are authorized to delete this group')} group = get_group_object(context, data_dict) - usergrps = model.User.get( user ).get_groups('publisher', 'admin') - - authorized = _groups_intersect( usergrps, group.get_groups('publisher') ) + userobj = model.User.get( user ) + if not userobj: + return {'success': False, 'msg': _('Only members of this group are authorized to delete this group')} + + authorized = _groups_intersect( userobj.get_groups('publisher', 'admin'), [group] ) if not authorized: return {'success': False, 'msg': _('User %s not authorized to delete group %s') % (str(user),group.id)} else: diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 691d965d502..ea88ea87d93 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -53,7 +53,6 @@ def group_update(context, data_dict): # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) - if not userobj: return {'success': False, 'msg': _('Could not find user %s') % str(user)} if not _groups_intersect( userobj.get_groups('publisher', 'admin'), [group] ): diff --git a/ckan/tests/functional/test_publisher_auth.py b/ckan/tests/functional/test_publisher_auth.py new file mode 100644 index 00000000000..614c6446a5f --- /dev/null +++ b/ckan/tests/functional/test_publisher_auth.py @@ -0,0 +1,102 @@ +import re + +from nose.tools import assert_equal + +import ckan.model as model +from ckan.lib.create_test_data import CreateTestData +from ckan.logic import NotAuthorized + + +from ckan.tests import * +from ckan.tests import setup_test_search_index +from base import FunctionalTestCase +from ckan.tests import search_related, is_search_supported + + +class TestPublisherGroups(FunctionalTestCase): + + @classmethod + def setup_class(self): + from ckan.tests.mock_publisher_auth import MockPublisherAuth + self.auth = MockPublisherAuth() + + model.Session.remove() + CreateTestData.create(auth_profile='publisher') + self.groupname = u'david' + self.packagename = u'testpkg' + model.repo.new_revision() + model.Session.add(model.Package(name=self.packagename)) + model.repo.commit_and_remove() + + @classmethod + def teardown_class(self): + model.Session.remove() + model.repo.rebuild_db() + model.Session.remove() + + def _run_fail_test( self, username, action): + grp = model.Group.by_name(self.groupname) + context = { 'group': grp, 'model': model, 'user': username } + try: + self.auth.check_access(action,context, {}) + assert False, "The user should not have access" + except NotAuthorized, e: + pass + + def _run_success_test( self, username, action): + userobj = model.User.get(username) + grp = model.Group.by_name(self.groupname) + f = model.User.get_groups + def gg(*args, **kwargs): + return [grp] + model.User.get_groups = gg + + context = { 'group': grp, 'model': model, 'user': username } + try: + self.auth.check_access(action, context, {}) + except NotAuthorized, e: + assert False, "The user should have %s access: %r." % (action, e.extra_msg) + model.User.get_groups = f + + def test_new_success(self): + self._run_success_test( 'russianfan', 'group_create' ) + + def test_new_fail(self): + self._run_fail_test( 'russianfan', 'group_create' ) + + def test_new_anon_fail(self): + self._run_fail_test( '', 'group_create' ) + + def test_new_unknown_fail(self): + self._run_fail_test( 'nosuchuser', 'group_create' ) + + def test_edit_success(self): + """ Success because user in group """ + self._run_success_test( 'russianfan', 'group_update' ) + + def test_edit_fail(self): + """ Fail because user not in group """ + self._run_fail_test( 'russianfan', 'group_update' ) + + def test_edit_anon_fail(self): + """ Fail because user is anon """ + self._run_fail_test( '', 'group_update' ) + + def test_edit_unknown_fail(self): + self._run_fail_test( 'nosuchuser', 'group_update' ) + + def test_delete_success(self): + """ Success because user in group """ + self._run_success_test( 'russianfan', 'group_delete' ) + + def test_delete_fail(self): + """ Fail because user not in group """ + self._run_fail_test( 'russianfan', 'group_delete' ) + + def test_delete_anon_fail(self): + """ Fail because user is anon """ + self._run_fail_test( '', 'group_delete' ) + + def test_delete_unknown_fail(self): + self._run_fail_test( 'nosuchuser', 'group_delete' ) + From 00e227fe2ab59ca52fef9da2f13585da2641f406 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 1 Feb 2012 10:48:39 +0000 Subject: [PATCH 21/59] [1669] Changes to add package auth tests and fixes based on those tests --- ckan/logic/auth/publisher/create.py | 16 +++- ckan/logic/auth/publisher/delete.py | 8 +- ckan/logic/auth/publisher/update.py | 25 +++--- ckan/tests/functional/test_group.py | 2 +- ckan/tests/functional/test_publisher_auth.py | 92 ++++++++++++++++++++ 5 files changed, 129 insertions(+), 14 deletions(-) diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index 5e9c1613c8e..61fcb8a85d3 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -9,8 +9,13 @@ def package_create(context, data_dict=None): model = context['model'] user = context['user'] - - return {'success': True} + userobj = model.User.get( user ) + + if userobj: + return {'success': True} + + return {'success': False, 'msg': 'You must be logged in to create a package'} + def resource_create(context, data_dict): return {'success': False, 'msg': 'Not implemented yet in the auth refactor'} @@ -32,6 +37,11 @@ def package_relationship_create(context, data_dict): return {'success': True} def group_create(context, data_dict=None): + """ + Group create permission. If a group is provided, within which we want to create a group + then we check that the user is within that group. If not then we just say Yes for now + although there may be some approval issues elsewhere. + """ model = context['model'] user = context['user'] @@ -39,6 +49,8 @@ def group_create(context, data_dict=None): return {'success': False, 'msg': _('User is not authorized to create groups') } try: + # If the user is doing this within another group then we need to make sure that + # the user has permissions for this group. group = get_group_object( context ) except NotFound: return { 'success' : True } diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index be08033ff56..c3d4b48131d 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -10,10 +10,12 @@ def package_delete(context, data_dict): model = context['model'] user = context['user'] package = get_package_object(context, data_dict) + packageobj = model.Package.by_name( package ) userobj = model.User.get( user ) if not userobj or \ - not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): + not packageobj or \ + not _groups_intersect( userobj.get_groups('publisher'), packageobj.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to delete packages in these group') % str(user)} return {'success': True} @@ -37,6 +39,10 @@ def relationship_delete(context, data_dict): def group_delete(context, data_dict): + """ + Group delete permission. Checks that the user specified is within the group to be deleted + and also have 'admin' capacity. + """ model = context['model'] user = context['user'] diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index ea88ea87d93..9c5cfb53b6b 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -12,13 +12,14 @@ def package_update(context, data_dict): model = context['model'] user = context.get('user') package = get_package_object(context, data_dict) -# group = get_group_object( context, data_dict ) + packageobj = model.Package.by_name( package ) -# userobj = model.User.get( user ) -# if not userobj or \ -# not _groups_intersect( userobj.get_groups('publisher'), [group] ): -# return {'success': False, -# 'msg': _('User %s not authorized to edit packages in these groups') % str(user)} + userobj = model.User.get( user ) + if not userobj or \ + not packageobj or \ + not _groups_intersect( userobj.get_groups('publisher'), packageobj.get_groups('publisher') ): + return {'success': False, + 'msg': _('User %s not authorized to edit packages in these groups') % str(user)} return {'success': True} @@ -44,6 +45,10 @@ def package_edit_permissions(context, data_dict): 'msg': _('Package edit permissions is not available')} def group_update(context, data_dict): + """ + Group edit permission. Checks that a valid user is supplied and that the user is + a member of the group currently with any capacity. + """ model = context['model'] user = context.get('user','') group = get_group_object(context, data_dict) @@ -54,11 +59,11 @@ def group_update(context, data_dict): # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) if not userobj: - return {'success': False, 'msg': _('Could not find user %s') % str(user)} - if not _groups_intersect( userobj.get_groups('publisher', 'admin'), [group] ): - return {'success': False, 'msg': _('User %s not authorized to edit this group') % str(user)} + return { 'success' : False, 'msg': _('Could not find user %s') % str(user) } + if not _groups_intersect( userobj.get_groups( 'publisher' ), [group] ): + return { 'success': False, 'msg': _('User %s not authorized to edit this group') % str(user) } - return {'success': True} + return { 'success': True } def group_change_state(context, data_dict): return group_update(context, data_dict) diff --git a/ckan/tests/functional/test_group.py b/ckan/tests/functional/test_group.py index 11da16311df..8b35e4d18e5 100644 --- a/ckan/tests/functional/test_group.py +++ b/ckan/tests/functional/test_group.py @@ -654,7 +654,7 @@ def gg(*args, **kwargs): context = { 'group': grp, 'model': model, 'user': 'russianfan' } try: - self.auth.check_access('group_update',context, {}): + self.auth.check_access('group_update',context, {}) except NotAuthorized, e: assert False, "The user should have access" diff --git a/ckan/tests/functional/test_publisher_auth.py b/ckan/tests/functional/test_publisher_auth.py index 614c6446a5f..160be2ea860 100644 --- a/ckan/tests/functional/test_publisher_auth.py +++ b/ckan/tests/functional/test_publisher_auth.py @@ -100,3 +100,95 @@ def test_delete_anon_fail(self): def test_delete_unknown_fail(self): self._run_fail_test( 'nosuchuser', 'group_delete' ) + +class TestPublisherGroupPackages(FunctionalTestCase): + + @classmethod + def setup_class(self): + from ckan.tests.mock_publisher_auth import MockPublisherAuth + self.auth = MockPublisherAuth() + + model.Session.remove() + CreateTestData.create(auth_profile='publisher') + self.groupname = u'david' + self.packagename = u'testpkg' + model.repo.new_revision() + model.Session.add(model.Package(name=self.packagename)) + model.repo.commit_and_remove() + + @classmethod + def teardown_class(self): + model.Session.remove() + model.repo.rebuild_db() + model.Session.remove() + + def _run_fail_test( self, username, action): + context = { 'package': self.packagename, 'model': model, 'user': username } + try: + self.auth.check_access(action, context, {}) + assert False, "The user should not have access" + except NotAuthorized, e: + pass + + def _run_success_test( self, username, action): + userobj = model.User.get(username) + grp = model.Group.by_name(self.groupname) + + f = model.User.get_groups + g = model.Package.get_groups + def gg(*args, **kwargs): + return [grp] + model.User.get_groups = gg + model.Package.get_groups = gg + + context = { 'package': self.packagename, 'model': model, 'user': username } + try: + self.auth.check_access(action, context, {}) + except NotAuthorized, e: + assert False, "The user should have %s access: %r." % (action, e.extra_msg) + model.User.get_groups = f + model.Package.get_groups = g + + def test_new_success(self): + self._run_success_test( 'russianfan', 'package_create' ) + + # Currently valid to have any logged in user succeed + #def test_new_fail(self): + # self._run_fail_test( 'russianfan', 'package_create' ) + + def test_new_anon_fail(self): + self._run_fail_test( '', 'package_create' ) + + def test_new_unknown_fail(self): + self._run_fail_test( 'nosuchuser', 'package_create' ) + + def test_edit_success(self): + """ Success because user in group """ + self._run_success_test( 'russianfan', 'package_update' ) + + def test_edit_fail(self): + """ Fail because user not in group """ + self._run_fail_test( 'russianfan', 'package_update' ) + + def test_edit_anon_fail(self): + """ Fail because user is anon """ + self._run_fail_test( '', 'package_update' ) + + def test_edit_unknown_fail(self): + self._run_fail_test( 'nosuchuser', 'package_update' ) + + def test_delete_success(self): + """ Success because user in group """ + self._run_success_test( 'russianfan', 'package_delete' ) + + def test_delete_fail(self): + """ Fail because user not in group """ + self._run_fail_test( 'russianfan', 'package_delete' ) + + def test_delete_anon_fail(self): + """ Fail because user is anon """ + self._run_fail_test( '', 'package_delete' ) + + def test_delete_unknown_fail(self): + self._run_fail_test( 'nosuchuser', 'package_delete' ) + From 354d910c22ea090c3684a963b23caf92c718568a Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 1 Feb 2012 11:44:28 +0000 Subject: [PATCH 22/59] [1669] Package relationship tests --- ckan/logic/auth/publisher/create.py | 15 ++-- ckan/logic/auth/publisher/update.py | 3 + ckan/tests/functional/test_publisher_auth.py | 74 ++++++++++++++++++++ 3 files changed, 88 insertions(+), 4 deletions(-) diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index 61fcb8a85d3..e2d1732a6fe 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -24,10 +24,17 @@ def package_relationship_create(context, data_dict): model = context['model'] user = context['user'] - id = data_dict['id'] - id2 = data_dict['id2'] - pkg1grps = model.Package.get(id).get_groups('publisher') - pkg2grps = model.Package.get(id2).get_groups('publisher') + id = data_dict.get('id', '') + id2 = data_dict.get('id2', '') + + pkg1 = model.Package.get(id) + pkg2 = model.Package.get(id2) + + if not pkg1 or not pkg2: + return {'success': False, 'msg': _('Two package IDs are required')} + + pkg1grps = pkg1.get_groups('publisher') + pkg2grps = pkg2.get_groups('publisher') usergrps = model.User.get( user ).get_groups('publisher') authorized = _groups_intersect( usergrps, pkg1grps ) and _groups_intersect( usergrps, pkg2grps ) diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 9c5cfb53b6b..35cb158aee7 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -29,6 +29,9 @@ def resource_update(context, data_dict): resource = get_resource_object(context, data_dict) userobj = model.User.get( user ) + if not userobj: + return {'success': False, 'msg': _('User %s not authorized to edit resources in this package') % str(user)} + if not _groups_intersect( userobj.get_groups('publisher'), resource.resource_group.package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to edit resources in this package') % str(user)} diff --git a/ckan/tests/functional/test_publisher_auth.py b/ckan/tests/functional/test_publisher_auth.py index 160be2ea860..06220645288 100644 --- a/ckan/tests/functional/test_publisher_auth.py +++ b/ckan/tests/functional/test_publisher_auth.py @@ -192,3 +192,77 @@ def test_delete_anon_fail(self): def test_delete_unknown_fail(self): self._run_fail_test( 'nosuchuser', 'package_delete' ) + +class TestPublisherPackageRelationships(FunctionalTestCase): + + @classmethod + def setup_class(self): + from ckan.tests.mock_publisher_auth import MockPublisherAuth + self.auth = MockPublisherAuth() + + model.Session.remove() + CreateTestData.create(auth_profile='publisher') + self.groupname = u'david' + self.package1name = u'testpkg' + self.package2name = u'testpkg2' + model.repo.new_revision() + model.Session.add(model.Package(name=self.package1name)) + model.Session.add(model.Package(name=self.package2name)) + model.repo.commit_and_remove() + + @classmethod + def teardown_class(self): + model.Session.remove() + model.repo.rebuild_db() + model.Session.remove() + + def test_create_fail_user( self): + p1 = model.Package.by_name( self.package1name ) + p2 = model.Package.by_name( self.package2name ) + + context = { 'model': model, 'user': 'russianfan' } + try: + self.auth.check_access('package_relationship_create', context, {'id': p1.id, 'id2': p2.id}) + assert False, "The user should not have access." + except NotAuthorized, e: + pass + + def test_create_fail_ddict( self): + p1 = model.Package.by_name( self.package1name ) + p2 = model.Package.by_name( self.package2name ) + + context = { 'model': model, 'user': 'russianfan' } + try: + self.auth.check_access('package_relationship_create', context, {'id': p1.id}) + assert False, "The user should not have access." + except NotAuthorized, e: + pass + + try: + self.auth.check_access('package_relationship_create', context, {'id2': p2.id}) + assert False, "The user should not have access." + except NotAuthorized, e: + pass + + def test_create_success(self): + userobj = model.User.get('russianfan') + + f = model.User.get_groups + g = model.Package.get_groups + def gg(*args, **kwargs): + return ['test_group'] + model.User.get_groups = gg + model.Package.get_groups = gg + + p1 = model.Package.by_name( self.package1name ) + p2 = model.Package.by_name( self.package2name ) + + context = { 'model': model, 'user': 'russianfan' } + try: + self.auth.check_access('package_relationship_create', context, {'id': p1.id, 'id2': p2.id}) + except NotAuthorized, e: + assert False, "The user should have %s access: %r." % (action, e.extra_msg) + model.User.get_groups = f + model.Package.get_groups = g + + From 995a388a9e96cb73b83a2c8e4c078bb00a8dfd85 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 1 Feb 2012 11:50:10 +0000 Subject: [PATCH 23/59] [1669] Test fixes to pass in the package object under the package key, not the name --- ckan/logic/auth/publisher/delete.py | 4 +--- ckan/logic/auth/publisher/update.py | 4 +--- ckan/tests/functional/test_publisher_auth.py | 10 ++++++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index c3d4b48131d..1a23e611fef 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -10,12 +10,10 @@ def package_delete(context, data_dict): model = context['model'] user = context['user'] package = get_package_object(context, data_dict) - packageobj = model.Package.by_name( package ) userobj = model.User.get( user ) if not userobj or \ - not packageobj or \ - not _groups_intersect( userobj.get_groups('publisher'), packageobj.get_groups('publisher') ): + not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to delete packages in these group') % str(user)} return {'success': True} diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 35cb158aee7..d5cadcc089e 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -12,12 +12,10 @@ def package_update(context, data_dict): model = context['model'] user = context.get('user') package = get_package_object(context, data_dict) - packageobj = model.Package.by_name( package ) userobj = model.User.get( user ) if not userobj or \ - not packageobj or \ - not _groups_intersect( userobj.get_groups('publisher'), packageobj.get_groups('publisher') ): + not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to edit packages in these groups') % str(user)} diff --git a/ckan/tests/functional/test_publisher_auth.py b/ckan/tests/functional/test_publisher_auth.py index 06220645288..1734437bc40 100644 --- a/ckan/tests/functional/test_publisher_auth.py +++ b/ckan/tests/functional/test_publisher_auth.py @@ -123,7 +123,8 @@ def teardown_class(self): model.Session.remove() def _run_fail_test( self, username, action): - context = { 'package': self.packagename, 'model': model, 'user': username } + pkg = model.Package.by_name(self.packagename) + context = { 'package': pkg, 'model': model, 'user': username } try: self.auth.check_access(action, context, {}) assert False, "The user should not have access" @@ -133,15 +134,16 @@ def _run_fail_test( self, username, action): def _run_success_test( self, username, action): userobj = model.User.get(username) grp = model.Group.by_name(self.groupname) - + pkg = model.Package.by_name(self.packagename) + f = model.User.get_groups g = model.Package.get_groups def gg(*args, **kwargs): return [grp] model.User.get_groups = gg model.Package.get_groups = gg - - context = { 'package': self.packagename, 'model': model, 'user': username } + + context = { 'package': pkg, 'model': model, 'user': username } try: self.auth.check_access(action, context, {}) except NotAuthorized, e: From ea708c72c20f9678f18808f81ee0605420b2973a Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 1 Feb 2012 12:04:11 +0000 Subject: [PATCH 24/59] [1669] Testing package_show --- ckan/logic/auth/publisher/get.py | 10 +++ ckan/tests/functional/test_publisher_auth.py | 78 ++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index e433be441b4..3f14e11be3b 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -66,6 +66,9 @@ def package_show(context, data_dict): package = get_package_object(context, data_dict) userobj = model.User.get( user ) + if not userobj: + return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} + if package.state == 'deleted': if not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} @@ -77,6 +80,13 @@ def resource_show(context, data_dict): user = context.get('user') resource = get_resource_object(context, data_dict) package = resource.revision_group.package + + if package.state == 'deleted': + userobj = model.User.get( user ) + if not userobj: + return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (str(user),package.id)} + if not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): + return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} pkg_dict = {'id': pkg.id} return package_show(context, pkg_dict) diff --git a/ckan/tests/functional/test_publisher_auth.py b/ckan/tests/functional/test_publisher_auth.py index 1734437bc40..b6ba20c94e3 100644 --- a/ckan/tests/functional/test_publisher_auth.py +++ b/ckan/tests/functional/test_publisher_auth.py @@ -101,6 +101,84 @@ def test_delete_unknown_fail(self): self._run_fail_test( 'nosuchuser', 'group_delete' ) +class TestPublisherShow(FunctionalTestCase): + + @classmethod + def setup_class(self): + from ckan.tests.mock_publisher_auth import MockPublisherAuth + self.auth = MockPublisherAuth() + + model.Session.remove() + CreateTestData.create(auth_profile='publisher') + self.groupname = u'david' + self.packagename = u'testpkg' + model.repo.new_revision() + model.Session.add(model.Package(name=self.packagename)) + model.repo.commit_and_remove() + + @classmethod + def teardown_class(self): + model.Session.remove() + model.repo.rebuild_db() + model.Session.remove() + + def test_package_show_deleted_success(self): + userobj = model.User.get('russianfan') + grp = model.Group.by_name(self.groupname) + pkg = model.Package.by_name(self.packagename) + pkg.state = 'deleted' + + f = model.User.get_groups + g = model.Package.get_groups + def gg(*args, **kwargs): + return [grp] + model.User.get_groups = gg + model.Package.get_groups = gg + + context = { 'package': pkg, 'model': model, 'user': userobj.name } + try: + self.auth.check_access('package_show', context, {}) + except NotAuthorized, e: + assert False, "The user should have %s access: %r." % (action, e.extra_msg) + model.User.get_groups = f + model.Package.get_groups = g + pkg.state = "active" + + def test_package_show_normal_success(self): + userobj = model.User.get('russianfan') + grp = model.Group.by_name(self.groupname) + pkg = model.Package.by_name(self.packagename) + pkg.state = "active" + + context = { 'package': pkg, 'model': model, 'user': userobj.name } + try: + self.auth.check_access('package_show', context, {}) + except NotAuthorized, e: + assert False, "The user should have %s access: %r." % ("package_show", e.extra_msg) + + def test_package_show_deleted_fail(self): + userobj = model.User.get('russianfan') + grp = model.Group.by_name(self.groupname) + pkg = model.Package.by_name(self.packagename) + pkg.state = 'deleted' + + g = model.Package.get_groups + def gg(*args, **kwargs): + return [grp] + model.Package.get_groups = gg + + context = { 'package': pkg, 'model': model, 'user': userobj.name } + try: + self.auth.check_access('package_show', context, {}) + assert False, "The user should not have access." + except NotAuthorized, e: + pass + model.Package.get_groups = g + pkg.state = "active" + + + + class TestPublisherGroupPackages(FunctionalTestCase): @classmethod From cf02a041faace942693bdbd5a381a4ca1ad5bc3a Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 1 Feb 2012 14:25:14 +0000 Subject: [PATCH 25/59] [1669] Removing unused relationship_delete auth function and adding PackageRelationship tests --- ckan/logic/auth/publisher/create.py | 4 +++ ckan/logic/auth/publisher/delete.py | 19 +++-------- ckan/logic/auth/publisher/get.py | 3 ++ ckan/logic/auth/publisher/update.py | 4 +++ ckan/tests/functional/test_publisher_auth.py | 36 ++++++++++++++++++-- 5 files changed, 49 insertions(+), 17 deletions(-) diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index e2d1732a6fe..bbe76cd087a 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -21,6 +21,10 @@ def resource_create(context, data_dict): return {'success': False, 'msg': 'Not implemented yet in the auth refactor'} def package_relationship_create(context, data_dict): + """ + Permission for users to create a new package relationship requires that the + user share a group with both packages. + """ model = context['model'] user = context['user'] diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index 1a23e611fef..c69b0090f32 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -7,6 +7,10 @@ from ckan.lib.base import _ def package_delete(context, data_dict): + """ + Delete a package permission. User must be in at least one group that that + package is also in. + """ model = context['model'] user = context['user'] package = get_package_object(context, data_dict) @@ -21,21 +25,6 @@ def package_delete(context, data_dict): def package_relationship_delete(context, data_dict): return package_relationship_create(context, data_dict) -def relationship_delete(context, data_dict): - model = context['model'] - user = context['user'] - relationship = context['relationship'] - - pkg1groups = set( relationship.package1.get_groups('publisher') ) - pkg2groups = set (relationship.package2.get_groups('publisher') ) - usergrps = model.User.get( user ).get_groups('publisher') - - if _groups_intersect( usergrps, pkg1groups ) and _groups_intersect( usergrps, pkg2groups ): - return {'success': True} - - return {'success': False, 'msg': _('User %s not authorized to delete relationship %s') % (str(user),relationship.id)} - - def group_delete(context, data_dict): """ Group delete permission. Checks that the user specified is within the group to be deleted diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index 3f14e11be3b..709c6769504 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -61,6 +61,7 @@ def package_relationships_list(context, data_dict): return {'success': True} def package_show(context, data_dict): + """ Package show permission checks the user group if the state is deleted """ model = context['model'] user = context.get('user') package = get_package_object(context, data_dict) @@ -76,6 +77,7 @@ def package_show(context, data_dict): return {'success': True} def resource_show(context, data_dict): + """ Resource show permission checks the user group if the package state is deleted """ model = context['model'] user = context.get('user') resource = get_resource_object(context, data_dict) @@ -97,6 +99,7 @@ def revision_show(context, data_dict): return {'success': True} def group_show(context, data_dict): + """ Group show permission checks the user group if the state is deleted """ model = context['model'] user = context.get('user') group = get_group_object(context, data_dict) diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index d5cadcc089e..c50253a369d 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -22,6 +22,10 @@ def package_update(context, data_dict): return {'success': True} def resource_update(context, data_dict): + """ + Update resource permission checks the user is in a group that the resource's + package is also a member of. + """ model = context['model'] user = context.get('user') resource = get_resource_object(context, data_dict) diff --git a/ckan/tests/functional/test_publisher_auth.py b/ckan/tests/functional/test_publisher_auth.py index b6ba20c94e3..7ea62157656 100644 --- a/ckan/tests/functional/test_publisher_auth.py +++ b/ckan/tests/functional/test_publisher_auth.py @@ -286,8 +286,18 @@ def setup_class(self): self.package1name = u'testpkg' self.package2name = u'testpkg2' model.repo.new_revision() - model.Session.add(model.Package(name=self.package1name)) - model.Session.add(model.Package(name=self.package2name)) + pkg1 = model.Package(name=self.package1name) + pkg2 = model.Package(name=self.package2name) + model.Session.add( pkg1 ) + model.Session.add( pkg2 ) + model.Session.flush() + pkg1 = model.Package.by_name(self.package1name) + pkg2 = model.Package.by_name(self.package2name) + + self.rel = model.PackageRelationship(name="test", type='depends_on') + self.rel.subject = pkg1 + self.rel.object = pkg2 + model.Session.add( self.rel ) model.repo.commit_and_remove() @classmethod @@ -344,5 +354,27 @@ def gg(*args, **kwargs): assert False, "The user should have %s access: %r." % (action, e.extra_msg) model.User.get_groups = f model.Package.get_groups = g + + def test_delete_success(self): + userobj = model.User.get('russianfan') + + f = model.User.get_groups + g = model.Package.get_groups + def gg(*args, **kwargs): + return ['test_group'] + model.User.get_groups = gg + model.Package.get_groups = gg + + p1 = model.Package.by_name( self.package1name ) + p2 = model.Package.by_name( self.package2name ) + + context = { 'model': model, 'user': 'russianfan', 'relationship': self.rel } + try: + self.auth.check_access('package_relationship_delete', context, {'id': p1.id, 'id2': p2.id }) + except NotAuthorized, e: + assert False, "The user should have %s access: %r." % ('package_relationship_delete', e.extra_msg) + + model.User.get_groups = f + model.Package.get_groups = g From c94ebfb6064d7cd39af7fedea5adf778c0bcf3a3 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 1 Feb 2012 14:47:17 +0000 Subject: [PATCH 26/59] [1669] Minor change to the test of auth function counts --- ckan/tests/misc/test_auth_profiles.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ckan/tests/misc/test_auth_profiles.py b/ckan/tests/misc/test_auth_profiles.py index b867aad232a..1ea985792c8 100644 --- a/ckan/tests/misc/test_auth_profiles.py +++ b/ckan/tests/misc/test_auth_profiles.py @@ -33,7 +33,7 @@ def test_authorizer_count(self): 'ckan.logic.auth': 0, 'ckan.logic.auth.publisher': 0 } - + for module_root in modules.keys(): for auth_module_name in ['get', 'create', 'update','delete']: module_path = '%s.%s' % (module_root, auth_module_name,) @@ -46,6 +46,6 @@ def test_authorizer_count(self): if not key.startswith('_'): modules[module_root] = modules[module_root] + 1 - # Differs based on auth imports - assert modules['ckan.logic.auth'] == modules['ckan.logic.auth.publisher'] - 3, modules + # The difference is the imported check_access_old in the default profile + assert modules['ckan.logic.auth'] == modules['ckan.logic.auth.publisher'] - 2, modules From 0cd06549ebb8bbb11e4f19be1ccbb1f4cd210ca0 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 1 Feb 2012 15:12:56 +0000 Subject: [PATCH 27/59] [1669] Cleaning up the imbalance in function count test that checks that any new functions added to auth are also added to auth.publisher --- ckan/logic/auth/publisher/create.py | 2 +- ckan/logic/auth/publisher/delete.py | 2 +- ckan/logic/auth/publisher/get.py | 3 ++- ckan/logic/auth/publisher/update.py | 2 +- ckan/tests/misc/test_auth_profiles.py | 3 +-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index bbe76cd087a..3631827826a 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -1,4 +1,4 @@ -from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ +from ckan.logic.auth import get_package_object, get_group_object, \ get_user_object, get_resource_object from ckan.logic.auth.publisher import _groups_intersect from ckan.logic import NotFound diff --git a/ckan/logic/auth/publisher/delete.py b/ckan/logic/auth/publisher/delete.py index c69b0090f32..57c9ee84260 100644 --- a/ckan/logic/auth/publisher/delete.py +++ b/ckan/logic/auth/publisher/delete.py @@ -1,4 +1,4 @@ -from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ +from ckan.logic.auth import get_package_object, get_group_object, \ get_user_object, get_resource_object from ckan.logic.auth import get_package_object, get_group_object from ckan.logic.auth.publisher import _groups_intersect diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index 709c6769504..1ecd89fb402 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -1,7 +1,8 @@ -from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ +from ckan.logic.auth import get_package_object, get_group_object, \ get_user_object, get_resource_object from ckan.lib.base import _ from ckan.logic.auth.publisher import _groups_intersect +from ckan.authz import Authorizer from ckan.logic.auth import get_package_object, get_group_object, get_resource_object diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index c50253a369d..70fff750b10 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -1,4 +1,4 @@ -from ckan.logic.auth import get_package_object, get_group_object, get_authorization_group_object, \ +from ckan.logic.auth import get_package_object, get_group_object,get_authorization_group_object, \ get_user_object, get_resource_object from ckan.logic.auth.publisher import _groups_intersect from ckan.logic.auth.publisher.create import package_relationship_create diff --git a/ckan/tests/misc/test_auth_profiles.py b/ckan/tests/misc/test_auth_profiles.py index 1ea985792c8..124cc0317d2 100644 --- a/ckan/tests/misc/test_auth_profiles.py +++ b/ckan/tests/misc/test_auth_profiles.py @@ -46,6 +46,5 @@ def test_authorizer_count(self): if not key.startswith('_'): modules[module_root] = modules[module_root] + 1 - # The difference is the imported check_access_old in the default profile - assert modules['ckan.logic.auth'] == modules['ckan.logic.auth.publisher'] - 2, modules + assert modules['ckan.logic.auth'] == modules['ckan.logic.auth.publisher'], modules From e37bcaa2485f924a8ab03ba7dcaadb5e24ab5e73 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 1 Feb 2012 17:18:36 +0000 Subject: [PATCH 28/59] [1669] Import cleanup --- ckan/logic/auth/create.py | 5 +++-- ckan/logic/auth/publisher/create.py | 5 ----- ckan/logic/auth/publisher/update.py | 2 +- ckan/logic/auth/update.py | 4 ++-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py index 19f06623178..011172f8fd9 100644 --- a/ckan/logic/auth/create.py +++ b/ckan/logic/auth/create.py @@ -12,7 +12,7 @@ def package_create(context, data_dict=None): return {'success': False, 'msg': _('User %s not authorized to create packages') % str(user)} else: - check2 = check_group_auth(context,data_dict) + check2 = _check_group_auth(context,data_dict) if not check2: return {'success': False, 'msg': _('User %s not authorized to edit these groups') % str(user)} @@ -73,7 +73,8 @@ def user_create(context, data_dict=None): else: return {'success': True} -def check_group_auth(context, data_dict): + +def _check_group_auth(context, data_dict): if not data_dict: return True diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index 3631827826a..7f3e2c407af 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -15,7 +15,6 @@ def package_create(context, data_dict=None): return {'success': True} return {'success': False, 'msg': 'You must be logged in to create a package'} - def resource_create(context, data_dict): return {'success': False, 'msg': 'Not implemented yet in the auth refactor'} @@ -88,10 +87,6 @@ def user_create(context, data_dict=None): return {'success': True} -def check_group_auth(context, data_dict): - # Maintained for function count in profiles, until we can rename to _* - return True - ## Modifications for rest api def package_create_rest(context, data_dict): diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index 70fff750b10..e6ec758b3a4 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -1,4 +1,4 @@ -from ckan.logic.auth import get_package_object, get_group_object,get_authorization_group_object, \ +from ckan.logic.auth import get_package_object, get_group_object, \ get_user_object, get_resource_object from ckan.logic.auth.publisher import _groups_intersect from ckan.logic.auth.publisher.create import package_relationship_create diff --git a/ckan/logic/auth/update.py b/ckan/logic/auth/update.py index 92ee545b9f1..7b84b430f2e 100644 --- a/ckan/logic/auth/update.py +++ b/ckan/logic/auth/update.py @@ -1,7 +1,7 @@ from ckan.logic import check_access_old, NotFound from ckan.logic.auth import get_package_object, get_resource_object, get_group_object, get_authorization_group_object, \ get_user_object, get_resource_object -from ckan.logic.auth.create import check_group_auth, package_relationship_create +from ckan.logic.auth.create import _check_group_auth, package_relationship_create from ckan.authz import Authorizer from ckan.lib.base import _ @@ -17,7 +17,7 @@ def package_update(context, data_dict): if not check1: return {'success': False, 'msg': _('User %s not authorized to edit package %s') % (str(user), package.id)} else: - check2 = check_group_auth(context,data_dict) + check2 = _check_group_auth(context,data_dict) if not check2: return {'success': False, 'msg': _('User %s not authorized to edit these groups') % str(user)} From cd6bf43592092a46d05a50aa911040d7cbecc52b Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Thu, 2 Feb 2012 17:12:09 +0000 Subject: [PATCH 29/59] [1669] Fixes for AlphaPage to make sure it is non-tag specific and handles lists of dicts better --- ckan/lib/alphabet_paginate.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ckan/lib/alphabet_paginate.py b/ckan/lib/alphabet_paginate.py index c467ee57158..f380eab3e37 100644 --- a/ckan/lib/alphabet_paginate.py +++ b/ckan/lib/alphabet_paginate.py @@ -21,7 +21,8 @@ from routes import url_for class AlphaPage(object): - def __init__(self, collection, alpha_attribute, page, other_text, paging_threshold=50): + def __init__(self, collection, alpha_attribute, page, other_text, paging_threshold=50, + url='/tag'): ''' @param collection - sqlalchemy query of all the items to paginate @param alpha_attribute - name of the attribute (on each item of the @@ -37,6 +38,7 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho self.page = page self.other_text = other_text self.paging_threshold = paging_threshold + self.url = url def pager(self, q=None): @@ -58,7 +60,7 @@ def pager(self, q=None): letters = [char for char in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'] + [self.other_text] for letter in letters: if letter != page: - page = HTML.a(class_='pager_link', href=url_for(controller='tag', action='index', page=letter), c=letter) + page = HTML.a(class_='pager_link', href=self.url + "?page=" + letter, c=letter) else: page = HTML.span(class_='pager_curpage', c=letter) pages.append(page) @@ -96,7 +98,10 @@ def items(self): elif isinstance(self.collection,list): if self.item_count >= self.paging_threshold: if self.page != self.other_text: - items = [x for x in self.collection if x[0:1].lower() == self.page.lower()] + if isinstance(self.collection[0], dict): + items = [x for x in self.collection if x[self.alpha_attribute][0:1].lower() == self.page.lower()] + else: + items = [x for x in self.collection if x[0:1].lower() == self.page.lower()] else: # regexp search items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x)] From 2a0ef6a91e25eed7be56533cf25d8d28f853f838 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Thu, 2 Feb 2012 17:29:30 +0000 Subject: [PATCH 30/59] [1669] A cleaner commit of the alpha pagination that takes the full controller path when that is how it is specified in routes. --- ckan/lib/alphabet_paginate.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ckan/lib/alphabet_paginate.py b/ckan/lib/alphabet_paginate.py index f380eab3e37..813c50a144f 100644 --- a/ckan/lib/alphabet_paginate.py +++ b/ckan/lib/alphabet_paginate.py @@ -22,7 +22,7 @@ class AlphaPage(object): def __init__(self, collection, alpha_attribute, page, other_text, paging_threshold=50, - url='/tag'): + controller_name='tag'): ''' @param collection - sqlalchemy query of all the items to paginate @param alpha_attribute - name of the attribute (on each item of the @@ -32,13 +32,17 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho non-alphabetic first character. @param paging_threshold - the minimum number of items required to start paginating them. + @param controller_name - The name of the controller that will be linked to, + which defaults to tag. The controller name should be the + same as the route so for some this will be the full + controller name such as 'A.B.controllers.C:Controller' ''' self.collection = collection self.alpha_attribute = alpha_attribute self.page = page self.other_text = other_text self.paging_threshold = paging_threshold - self.url = url + self.controller_name = controller_name def pager(self, q=None): @@ -60,7 +64,7 @@ def pager(self, q=None): letters = [char for char in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'] + [self.other_text] for letter in letters: if letter != page: - page = HTML.a(class_='pager_link', href=self.url + "?page=" + letter, c=letter) + page = HTML.a(class_='pager_link', href=url_for(controller=self.controller_name, action='index', page=letter),c=letter) else: page = HTML.span(class_='pager_curpage', c=letter) pages.append(page) From d0e2c43983e35f6a594b18577465a4b939176452 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 3 Feb 2012 09:47:14 +0000 Subject: [PATCH 31/59] [1669] Fixes to the unknown char handling in alpha page --- ckan/lib/alphabet_paginate.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ckan/lib/alphabet_paginate.py b/ckan/lib/alphabet_paginate.py index 813c50a144f..0fe764258de 100644 --- a/ckan/lib/alphabet_paginate.py +++ b/ckan/lib/alphabet_paginate.py @@ -35,7 +35,7 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho @param controller_name - The name of the controller that will be linked to, which defaults to tag. The controller name should be the same as the route so for some this will be the full - controller name such as 'A.B.controllers.C:Controller' + controller name such as 'A.B.controllers.C:ClassName' ''' self.collection = collection self.alpha_attribute = alpha_attribute @@ -64,10 +64,10 @@ def pager(self, q=None): letters = [char for char in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'] + [self.other_text] for letter in letters: if letter != page: - page = HTML.a(class_='pager_link', href=url_for(controller=self.controller_name, action='index', page=letter),c=letter) + page_element = HTML.a(class_='pager_link', href=url_for(controller=self.controller_name, action='index', page=letter),c=letter) else: - page = HTML.span(class_='pager_curpage', c=letter) - pages.append(page) + page_element = HTML.span(class_='pager_curpage', c=letter) + pages.append(page_element) div = HTML.tag('div', class_='pager', *pages) return div @@ -108,7 +108,10 @@ def items(self): items = [x for x in self.collection if x[0:1].lower() == self.page.lower()] else: # regexp search - items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x)] + if isinstance(self.collection[0], dict): + items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x[self.alpha_attribute])] + else: + items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x)] items.sort() else: items = self.collection From 3c0e7bfb15a7f1e1566da41a7a9331a48f6fb5c6 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 6 Feb 2012 13:29:25 +0000 Subject: [PATCH 32/59] [1669] Fixed the get_groups lookup to make sure it works as expected --- ckan/model/group.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/ckan/model/group.py b/ckan/model/group.py index b80b932c40f..6636236f921 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -51,6 +51,16 @@ def __init__(self, group=None, table_id=None, group_id=None, self.capacity = capacity self.state = state + @classmethod + def get(cls, reference): + '''Returns a group object referenced by its id or name.''' + query = Session.query(cls).filter(cls.id==reference) + member = query.first() + if member == None: + member = cls.by_name(reference) + return member + + def get_related(self, type): """ TODO: Determine if this is useful Get all objects that are members of the group of the specified type. @@ -159,11 +169,11 @@ def get_groups(self, group_type=None, capacity=None): import ckan.model as model if '_groups' not in self.__dict__: self._groups = model.Session.query(model.Group).\ - join(model.Member, model.Member.group_id == self.id and \ + join(model.Member, model.Member.group_id == model.Group.id and \ model.Member.table_name == 'group').\ filter(model.Member.state == 'active').\ filter(model.Member.table_id == self.id).all() - + groups = self._groups if group_type: groups = [g for g in groups if g.type == group_type] From 8f1e7f6ee3ef4b78fad154961d67a433c3e78ffe Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 7 Feb 2012 13:14:13 +0000 Subject: [PATCH 33/59] [1669] Moved my migration and fixed the lookup in alphabet_paginate for when it is working on objects. --- ckan/lib/alphabet_paginate.py | 2 +- ...oup_approval_status.py => 0049_add_group_approval_status.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename ckan/migration/versions/{0048_add_group_approval_status.py => 0049_add_group_approval_status.py} (100%) diff --git a/ckan/lib/alphabet_paginate.py b/ckan/lib/alphabet_paginate.py index 0fe764258de..a4ae541247b 100644 --- a/ckan/lib/alphabet_paginate.py +++ b/ckan/lib/alphabet_paginate.py @@ -105,7 +105,7 @@ def items(self): if isinstance(self.collection[0], dict): items = [x for x in self.collection if x[self.alpha_attribute][0:1].lower() == self.page.lower()] else: - items = [x for x in self.collection if x[0:1].lower() == self.page.lower()] + items = [x for x in self.collection if getattr(x,self.alpha_attribute)[0:1].lower() == self.page.lower()] else: # regexp search if isinstance(self.collection[0], dict): diff --git a/ckan/migration/versions/0048_add_group_approval_status.py b/ckan/migration/versions/0049_add_group_approval_status.py similarity index 100% rename from ckan/migration/versions/0048_add_group_approval_status.py rename to ckan/migration/versions/0049_add_group_approval_status.py From 263115bbd504759d4a43364af65c7b0712e33920 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 8 Feb 2012 09:35:14 +0000 Subject: [PATCH 34/59] [1669] Changed alphabet pagination to now show empty letters as links. --- ckan/lib/alphabet_paginate.py | 10 +++++++++- ckan/public/css/style.css | 1 - 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ckan/lib/alphabet_paginate.py b/ckan/lib/alphabet_paginate.py index a4ae541247b..75979b3fbad 100644 --- a/ckan/lib/alphabet_paginate.py +++ b/ckan/lib/alphabet_paginate.py @@ -43,6 +43,11 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho self.other_text = other_text self.paging_threshold = paging_threshold self.controller_name = controller_name + self.available = dict( (c,0,) for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) + for c in self.collection: + x = getattr(c, self.alpha_attribute)[0] + self.available[x] = self.available.get(x, 0) + 1 + def pager(self, q=None): @@ -64,7 +69,10 @@ def pager(self, q=None): letters = [char for char in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'] + [self.other_text] for letter in letters: if letter != page: - page_element = HTML.a(class_='pager_link', href=url_for(controller=self.controller_name, action='index', page=letter),c=letter) + if self.available.get(letter, 0): + page_element = HTML.a(class_='pager_link', href=url_for(controller=self.controller_name, action='index', page=letter),c=letter) + else: + page_element = HTML.span(class_="pager_empty", c=letter) else: page_element = HTML.span(class_='pager_curpage', c=letter) pages.append(page_element) diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 47eccf7c36a..140335ac9ff 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -431,7 +431,6 @@ tbody tr.table-empty td { border: 1px solid #ddd; } - /* ====== */ /* Facets */ /* ====== */ From 40a62a49953dafdd7d3a9eba833a15f0f67a43ad Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 8 Feb 2012 11:14:36 +0000 Subject: [PATCH 35/59] [1669] Enabled sysadmin group editing and enforced required admin membership of group for editing --- ckan/logic/auth/publisher/update.py | 10 ++++-- .../templates/publisher_form.html | 31 ++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index e6ec758b3a4..bf802334dd0 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -57,15 +57,21 @@ def group_update(context, data_dict): model = context['model'] user = context.get('user','') group = get_group_object(context, data_dict) - + if not user: return {'success': False, 'msg': _('Only members of this group are authorized to edit this group')} + + # Sys admins should be allowed to update groups + if Authorizer().is_sysadmin(unicode(user)): + return { 'success': True } # Only allow package update if the user and package groups intersect userobj = model.User.get( user ) if not userobj: return { 'success' : False, 'msg': _('Could not find user %s') % str(user) } - if not _groups_intersect( userobj.get_groups( 'publisher' ), [group] ): + + # Only admins of this group should be able to update this group + if not _groups_intersect( userobj.get_groups( 'publisher', 'admin' ), [group] ): return { 'success': False, 'msg': _('User %s not authorized to edit this group') % str(user) } return { 'success': True } diff --git a/ckanext/publisher_form/templates/publisher_form.html b/ckanext/publisher_form/templates/publisher_form.html index bd9eca57d65..89bb61571e5 100644 --- a/ckanext/publisher_form/templates/publisher_form.html +++ b/ckanext/publisher_form/templates/publisher_form.html @@ -82,12 +82,12 @@

        Extras

        Users

        -
        +
        -
        +
        Admin Editor
        @@ -95,16 +95,18 @@

        Users

        There are no users currently in this publisher.

        - -

        Add users

        -
        -
        -
        - Admin - Editor -
        -
        -
        +Super? ${c.is_superuser_or_groupadmin} + +

        Add users

        +
        +
        +
        + Admin + Editor +
        +
        +
        +

        @@ -125,6 +127,11 @@

        Add datasets

        +
        +
        + Add a new dataset +
        +
        From d7454573e917da7bb96ece5988bf383b1c507a35 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 8 Feb 2012 12:03:03 +0000 Subject: [PATCH 36/59] [1669] Removing debugging --- ckanext/publisher_form/templates/publisher_form.html | 1 - 1 file changed, 1 deletion(-) diff --git a/ckanext/publisher_form/templates/publisher_form.html b/ckanext/publisher_form/templates/publisher_form.html index 89bb61571e5..b9fcbc63dbb 100644 --- a/ckanext/publisher_form/templates/publisher_form.html +++ b/ckanext/publisher_form/templates/publisher_form.html @@ -95,7 +95,6 @@

        Users

        There are no users currently in this publisher.

        -Super? ${c.is_superuser_or_groupadmin}

        Add users

        From cd8405adcf291806a789d233cff33e2d20990f2b Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 8 Feb 2012 15:16:53 +0000 Subject: [PATCH 37/59] [1669] Make sure we only allow adding a dataset when we are editing, not when we are creating a new publisher --- ckanext/publisher_form/templates/publisher_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/publisher_form/templates/publisher_form.html b/ckanext/publisher_form/templates/publisher_form.html index b9fcbc63dbb..45cb350b6fb 100644 --- a/ckanext/publisher_form/templates/publisher_form.html +++ b/ckanext/publisher_form/templates/publisher_form.html @@ -126,7 +126,7 @@

        Add datasets

        -
        +
        Add a new dataset
        From 08c21df25270842fed9990cea75583aafe80c6a6 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Fri, 10 Feb 2012 01:06:43 +0000 Subject: [PATCH 38/59] [master,css][xs]: nicer gravatar styling. --- ckan/public/css/style.css | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css index 394ddba620f..f5d935ed300 100644 --- a/ckan/public/css/style.css +++ b/ckan/public/css/style.css @@ -78,12 +78,6 @@ header .account { float: right; } -header .account img.gravatar { - margin: 0 5px -5px 0; - border-radius: 3px; -} - - header .search { margin: 1px; margin-left: 1em; @@ -238,6 +232,10 @@ tbody tr.table-empty td { border-bottom: 1px dashed #000; } +img.gravatar { + margin: 0 5px -5px 0; + border-radius: 3px; +} /* =============== */ /* MinorNavigation */ From 73f2b028531f2167b897e5c7fd6da44fb14a0453 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 10 Feb 2012 10:46:38 +0000 Subject: [PATCH 39/59] [1669] Fix to the package show logic for deleted packages --- ckan/logic/auth/publisher/get.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index 1ecd89fb402..fffc9eb7308 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -64,14 +64,18 @@ def package_relationships_list(context, data_dict): def package_show(context, data_dict): """ Package show permission checks the user group if the state is deleted """ model = context['model'] - user = context.get('user') package = get_package_object(context, data_dict) - userobj = model.User.get( user ) - - if not userobj: - return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} if package.state == 'deleted': + user = context.get('user') + + if not user: + return {'success': False, 'msg': _('User not authorized to read package %s') % (package.id)} + + userobj = model.User.get( user ) + if not userobj: + return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} + if not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): return {'success': False, 'msg': _('User %s not authorized to read package %s') % (str(user),package.id)} From 495be3eb8a8f19cc5a083fb0f7c9fe2ace12af56 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Fri, 10 Feb 2012 10:56:33 +0000 Subject: [PATCH 40/59] [xs] Change the building of search indices to only pull active packages --- ckan/lib/search/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/lib/search/__init__.py b/ckan/lib/search/__init__.py index 0963e2301d5..2c432b98acc 100644 --- a/ckan/lib/search/__init__.py +++ b/ckan/lib/search/__init__.py @@ -127,7 +127,7 @@ def rebuild(package=None): else: # rebuild index package_index.clear() - for pkg in model.Session.query(model.Package).all(): + for pkg in model.Session.query(model.Package).filter(model.Package.state == 'active').all(): package_index.insert_dict( get_action('package_show_rest')( {'model': model, 'ignore_auth': True}, From 059040f71dbe09bae27c42ba771ccd0c994f241a Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 10 Feb 2012 16:09:41 +0000 Subject: [PATCH 41/59] [release-v1.6][[doc]: Renamed doc to be more helpful. --- doc/{plugins.rst => writing-extensions.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename doc/{plugins.rst => writing-extensions.rst} (100%) diff --git a/doc/plugins.rst b/doc/writing-extensions.rst similarity index 100% rename from doc/plugins.rst rename to doc/writing-extensions.rst From e9c315f289f513125a102f6711dee407ff52ffa3 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 10 Feb 2012 16:12:10 +0000 Subject: [PATCH 42/59] [release-v1.6][doc]: Update docs generally. --- doc/common-error-messages.rst | 9 +++- doc/extensions.rst | 20 ++++----- doc/index.rst | 5 +-- doc/install-from-source.rst | 77 ++++++++++++++++------------------- doc/paster.rst | 49 +++++++++++++++++++++- doc/prepare-extensions.rst | 33 --------------- 6 files changed, 102 insertions(+), 91 deletions(-) delete mode 100644 doc/prepare-extensions.rst diff --git a/doc/common-error-messages.rst b/doc/common-error-messages.rst index 468bfcea488..f3c7fea7ff2 100644 --- a/doc/common-error-messages.rst +++ b/doc/common-error-messages.rst @@ -132,4 +132,11 @@ This occurs when upgrading to CKAN 1.5.1 with a database with duplicate user nam ``ERROR: must be member of role "okfn"`` & ``WARNING: no privileges could be revoked for "public"`` ===================================================================================================== -These are seen when loading a CKAN database from another machine. It is the result of the database tables being owned by a user that doesn't exist on the new machine. The owner of the table is not important, so this error is harmless and can be ignored. \ No newline at end of file +These are seen when loading a CKAN database from another machine. It is the result of the database tables being owned by a user that doesn't exist on the new machine. The owner of the table is not important, so this error is harmless and can be ignored. + +``IOError: [Errno 13] Permission denied: '/var/log/ckan/colorado/colorado.log'`` +================================================================================ + +This is usually seen when you run the paster command with one user, and CKAN is deployed on Apache (for example) which runs as another user. The usual remedy is to run the paster command as user ``www-data``. i.e..:: + +sudo -u www-data paster ... \ No newline at end of file diff --git a/doc/extensions.rst b/doc/extensions.rst index 2fe93dab087..e5a3bc87605 100644 --- a/doc/extensions.rst +++ b/doc/extensions.rst @@ -6,10 +6,10 @@ This is where it gets interesting! The CKAN software can be customised with 'ext Extensions allow you to customise CKAN for your own requirements, without interfering with the basic CKAN system. -.. warning:: This is an advanced topic. At the moment, you need to have prepared your system to work with extensions, as described in :doc:`prepare-extensions`. We are working to make the most popular extensions more easily available as Debian packages. +.. warning:: This is an advanced topic. -Finding Extensions ------------------- +Choosing Extensions +------------------- All CKAN extensions are listed on the official `Extension listing on the CKAN wiki `_. @@ -37,25 +37,23 @@ You can install an extension on a CKAN instance as follows. .. note:: - Core extensions do not need to be installed -- just enabled (see below). + 'Core' extensions do not need to be installed -- just enabled (see below). -1. First, ensure you are working within your virtualenv (see :doc:`prepare-extensions` if you are not sure what this means):: +#. Locate your CKAN virtual environment (pyenv) in your filesystem. It is usually in a directory similar to this: ``/var/lib/ckan/INSTANCE_NAME/pyenv`` - . /home/ubuntu/pyenv/bin/activate +If it is not here, to get the definitive answer, check your CKAN Apache configuration (``/etc/apache2/sites-enabled``) for your WSGIScriptAlias (e.g. ``/var/lib/ckan/colorado/wsgi.py``) which has an ``execfile`` instruction. The first parameter is the pyenv directory plus ``/bin/activate_this.py``. e.g. ``/var/lib/ckan/colorado/pyenv/bin/activate_this.py`` means the pyenv dir is: ``/var/lib/ckan/colorado/pyenv``. -2. Install the extension package code using ``pip``. +#. Install the extension package code into your pyenv using ``pip``. For example, to install the Disqus extension, which allows users to comment on datasets:: - pip install -E ~/var/srvc/ckan.net/pyenv git+https://github.com/okfn/ckanext-disqus.git - - The ``-E`` parameter is for your CKAN Python environment (e.g. ``~/var/srvc/ckan.net/pyenv``). + /var/lib/ckan/INSTANCE_NAME/pyenv/bin/pip install -E /var/lib/ckan/INSTANCE_NAME/pyenv -e git+https://github.com/okfn/ckanext-disqus.git#egg=ckanext-disqus Prefix the source URL with the repo type (``hg+`` for Mercurial, ``git+`` for Git). The dependency you've installed will appear in the ``src/`` directory under your Python environment. -Now the extension is installed you need to enable it. +Now the extension is installed, so now you can enable it. Enabling an Extension diff --git a/doc/index.rst b/doc/index.rst index 481947dd850..7258f9a3502 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -6,7 +6,7 @@ This Administration Guide covers how to set up and manage `CKAN `_. @@ -23,9 +23,8 @@ Contents: loading_data paster authorization - prepare-extensions extensions - plugins + writing-extensions forms form-integration database_dumps diff --git a/doc/install-from-source.rst b/doc/install-from-source.rst index 27c98ac1a57..884d4b715a0 100644 --- a/doc/install-from-source.rst +++ b/doc/install-from-source.rst @@ -150,72 +150,65 @@ locations: 6. Setup a PostgreSQL database ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - List existing databases: +List existing databases: :: -sudo -u postgres psql -l + sudo -u postgres psql -l - It is advisable to ensure that the encoding of databases is 'UTF8', or - internationalisation may be a problem. Since changing the encoding of PostgreSQL - may mean deleting existing databases, it is suggested that this is fixed before - continuing with the CKAN install. +It is advisable to ensure that the encoding of databases is 'UTF8', or +internationalisation may be a problem. Since changing the encoding of PostgreSQL +may mean deleting existing databases, it is suggested that this is fixed before +continuing with the CKAN install. - Next you'll need to create a database user if one doesn't already exist. +Next you'll need to create a database user if one doesn't already exist. .. tip :: -If you choose a database name, user or password which are different from the example values suggested below then you'll need to change the sqlalchemy.url value accordingly in the CKAN configuration file you'll create in the next step. + If you choose a database name, user or password which are different from the example values suggested below then you'll need to change the sqlalchemy.url value accordingly in the CKAN configuration file that you'll create in the next step. - Here we create a user called ``ckanuser`` and will enter ``pass`` for the password when prompted: +Here we create a user called ``ckanuser`` and will enter ``pass`` for the password when prompted: :: -sudo -u postgres createuser -S -D -R -P ckanuser + sudo -u postgres createuser -S -D -R -P ckanuser - Now create the database (owned by ``ckanuser``), which we'll call ``ckantest``: +Now create the database (owned by ``ckanuser``), which we'll call ``ckantest``: :: -sudo -u postgres createdb -O ckanuser ckantest + sudo -u postgres createdb -O ckanuser ckantest 7. Create a CKAN config file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Make sure you are in an activated environment (see step 3) so that Python - Paste and other modules are put on the python path (your command prompt will - start with ``(pyenv)`` if you have) then change into the ``ckan`` directory - which will have been created when you installed CKAN in step 4 and create the - CKAN config file using Paste. These instructions call it ``development.ini`` since that is the required name for running the CKAN tests. But for a server deployment then you might want to call it say after the server hostname e.g. ``test.ckan.net.ini``. +Make sure you are in an activated environment (see step 3) so that Python +Paste and other modules are put on the python path (your command prompt will +start with ``(pyenv)`` if you have) then change into the ``ckan`` directory +which will have been created when you installed CKAN in step 4 and create the +CKAN config file using Paste. These instructions call it ``development.ini`` since that is the required name for running the CKAN tests. But for a server deployment then you might want to call it say after the server hostname e.g. ``test.ckan.net.ini``. :: -cd pyenv/src/ckan -paster make-config ckan development.ini + cd pyenv/src/ckan + paster make-config ckan development.ini - If you used a different database name or password when creating the database - in step 5 you'll need to now edit ``development.ini`` and change the - ``sqlalchemy.url`` line, filling in the database name, user and password you used. +If you used a different database name or password when creating the database +in step 5 you'll need to now edit ``development.ini`` and change the +``sqlalchemy.url`` line, filling in the database name, user and password you used. :: -sqlalchemy.url = postgresql://ckanuser:pass@localhost/ckantest + sqlalchemy.url = postgresql://ckanuser:pass@localhost/ckantest - If you're using a remote host with password authentication rather than SSL authentication, use:: +If you're using a remote host with password authentication rather than SSL authentication, use:: -sqlalchemy.url = postgresql://:@/ckan?sslmode=disable + sqlalchemy.url = postgresql://:@/ckan?sslmode=disable - .. caution :: +.. caution :: - Advanced users: If you have installed CKAN using the Fabric file capability (deprecated), - your config file will be called something like ``pyenv/ckan.net.ini``. - This is fine but CKAN probably won't be - able to find your ``who.ini`` file. To fix this edit ``pyenv/ckan.net.ini``, - search for the line ``who.config_file = %(here)s/who.ini`` and change it - to ``who.config_file = who.ini``. + Legacy installs of CKAN may have the config file in the pyenv directory, e.g. ``pyenv/ckan.net.ini``. This is fine but CKAN probably won't be able to find your ``who.ini`` file. To fix this edit ``pyenv/ckan.net.ini``, search for the line ``who.config_file = %(here)s/who.ini`` and change it to ``who.config_file = who.ini``. - We are moving to a new deployment system where this incompatibility - will be fixed. 8. Create database tables ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -243,14 +236,14 @@ sqlalchemy.url = postgresql://:@/ckan?sslmode=disabl 9. Create the cache directory ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - You need to create the Pylon's cache directory specified by 'cache_dir' - in the config file. +You need to create the Pylon's cache directory specified by 'cache_dir' +in the config file. - (from the ``pyenv/src/ckan`` directory): +(from the ``pyenv/src/ckan`` directory): :: -mkdir data + mkdir data 10. Setup Solr @@ -262,8 +255,8 @@ Set appropriate values for the ``ckan.site_id`` and ``solr_url`` config variable :: - ckan.site_id=my_ckan_instance - solr_url=http://127.0.0.1:8983/solr + ckan.site_id=my_ckan_instance + solr_url=http://127.0.0.1:8983/solr @@ -277,12 +270,12 @@ Set appropriate values for the ``ckan.site_id`` and ``solr_url`` config variable :: -paster serve development.ini + paster serve development.ini 12. Point your web browser at: http://127.0.0.1:5000/ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - The CKAN homepage should load. +The CKAN homepage should load. Finally, make sure that tests pass, as described in :ref:`basic-tests`. diff --git a/doc/paster.rst b/doc/paster.rst index 8d6f4f024a3..b73ee6950d3 100644 --- a/doc/paster.rst +++ b/doc/paster.rst @@ -14,14 +14,61 @@ Paster is run on the command line on the server running CKAN. This section cover Understanding Paster ==================== -The basic paster format is:: +At its simplest, paster commands can be thought of like this:: + + paster + +But there are various extra elements to the commandline that usually need adding. We shall build them up: + +#. Enabling CKAN commands +========================= + +Paster is used for many things aside from CKAN. You usually need to tell paster that you want to enable the CKAN commands:: + + paster --plugin=ckan + +You know you need to do this if you get the error ``Command 'user' not known`` for a valid CKAN command. + +(Alternatively, CKAN commands are enabled by default if your current directory is the CKAN source directory) + +#. Pointing to your CKAN config +=============================== + +Paster needs to know where your CKAN config file is (so it knows which database and search index to deal with etc.):: paster --plugin=ckan --config= +If you forget to specify ``--config`` then you will get error ``AssertionError: Config filename '/home/okfn/development.ini' does not exist.`` + +(Paster defaults to looking for development.ini in the current directory.) + For example, to initialise a database:: paster --plugin=ckan db init --config=/etc/ckan/std/std.ini +#. Virtual environments +======================= + +You often need to run paster within your CKAN virtual environment (pyenv). If CKAN was installed as 'source' then you can activate it as usual before running the paster command:: + + . ~/pyenv/bin/activate + paster --plugin=ckan db init --config=/etc/ckan/std/std.ini + +The alternative, which also suits a CKAN 'package' install, is to simply give the full path to the paster in your pyenv:: + + /var/lib/ckan/std/pyenv/bin/paster --plugin=ckan db init --config=/etc/ckan/std/std.ini + + +#. Running Paster on a deployment +================================= + +If CKAN is deployed with Apache on this machine, then you should run paster as the same user, which is usually ``www-data``. This is because paster will write to the same CKAN logfile as the Apache process and file permissions need to match. + + For example:: + + sudo -u www-data /var/lib/ckan/std/pyenv/bin/paster --plugin=ckan db init --config=/etc/ckan/std/std.ini + +Otherwise you will get an error such as: ``IOError: [Errno 13] Permission denied: '/var/log/ckan/std/std.log'``. .. _paster-help: diff --git a/doc/prepare-extensions.rst b/doc/prepare-extensions.rst deleted file mode 100644 index 496c54c54d9..00000000000 --- a/doc/prepare-extensions.rst +++ /dev/null @@ -1,33 +0,0 @@ -========================= -Prepare to Use Extensions -========================= - -If you are running a package installation of CKAN, before you start using and testing extensions (described in :doc:`extensions`) you need to prepare your system. - -Firstly, you'll need to set up and enter a virtual Python environment, as follows: - -:: - - # install software we need (virtualenv and git to retrieve the source code) - sudo apt-get install python-virtualenv git-core - # create a python virtual env and activate - virtualenv /home/ubuntu/pyenv - . /home/ubuntu/pyenv/bin/activate - -Then, you need to install the CKAN source into your virtual environment. You can install CKAN like this: - -:: - - pip install -e git+http://github.com/okfn/ckan#egg=ckan - -Your new CKAN developer install will be running on http://localhost:5000/ - -When you start using extensions, you should install any of the developer versions of the CKAN extensions you want to work on like this (using the appropriate URL): - -:: - - pip install -e git+http://github.com/okfn/@#egg= - -The dependency you've installed will appear in ``/home/ubuntu/pyenv/src/`` where you can work on it. - -After working on extensions, you should make sure that your deployment passes the tests, as described in :doc:`test`. From 241687f5735c159bc66b97e95f570a47986db187 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 10 Feb 2012 17:42:56 +0000 Subject: [PATCH 43/59] [master][version]: Increment version now we have branched for release-v1.6. --- ckan/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/__init__.py b/ckan/__init__.py index bdfc574f294..9bdf93a52a7 100644 --- a/ckan/__init__.py +++ b/ckan/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.5.2a' +__version__ = '1.6.1a' __description__ = 'Comprehensive Knowledge Archive Network (CKAN) Software' __long_description__ = \ '''CKAN software provides a hub for datasets. The flagship site running CKAN From d3cd286aeba98fdee58abf8fdc56c459a13a0161 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 13 Feb 2012 10:24:08 +0000 Subject: [PATCH 44/59] [1669] Allow ignore_auth to be specified in context for package_show --- ckan/logic/auth/publisher/get.py | 3 +++ ckan/logic/auth/publisher/update.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/ckan/logic/auth/publisher/get.py b/ckan/logic/auth/publisher/get.py index fffc9eb7308..7766b1ae1fa 100644 --- a/ckan/logic/auth/publisher/get.py +++ b/ckan/logic/auth/publisher/get.py @@ -67,6 +67,9 @@ def package_show(context, data_dict): package = get_package_object(context, data_dict) if package.state == 'deleted': + if 'ignore_auth' in context and context['ignore_auth']: + return {'success': True} + user = context.get('user') if not user: diff --git a/ckan/logic/auth/publisher/update.py b/ckan/logic/auth/publisher/update.py index bf802334dd0..cc8ca288f9b 100644 --- a/ckan/logic/auth/publisher/update.py +++ b/ckan/logic/auth/publisher/update.py @@ -13,6 +13,9 @@ def package_update(context, data_dict): user = context.get('user') package = get_package_object(context, data_dict) + if Authorizer().is_sysadmin(unicode(user)): + return { 'success': True } + userobj = model.User.get( user ) if not userobj or \ not _groups_intersect( userobj.get_groups('publisher'), package.get_groups('publisher') ): From 55c8ff6f108a1b9706b01ea68d875042639bfa9e Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 13 Feb 2012 11:02:09 +0000 Subject: [PATCH 45/59] [#1359,licenses][s]: refactor to remove dependency on external licenses package. * Inline list of default licenses (those from http://licenses.opendefinition.org/licenses/groups/ckan.json) * Minor refactoring on initialization of LicenseRegister to support dicts or lists or licenses * Fix up tests (now that we do not have gpl in our default lists of licenses) --- ckan/lib/create_test_data.py | 2 +- ckan/model/license.py | 237 +++++++++++++++++++++++++- ckan/tests/functional/test_package.py | 4 +- ckan/tests/models/test_license.py | 1 - ckan/tests/models/test_package.py | 6 +- requires/lucid_missing.txt | 1 - 6 files changed, 237 insertions(+), 14 deletions(-) diff --git a/ckan/lib/create_test_data.py b/ckan/lib/create_test_data.py index 1a45b9ad86c..6554cf8371a 100644 --- a/ckan/lib/create_test_data.py +++ b/ckan/lib/create_test_data.py @@ -546,7 +546,7 @@ def get_all_data(cls): 'format':'DOC', 'description':'http://www.statistics.gov.uk/hub/id/119-34565'}], 'groups':'ukgov test1 test2 penguin', - 'license':'gpl-3.0', + 'license':'odc-by', 'notes':u'''From > The Government Information Locator Service (GILS) is an effort to identify, locate, and describe publicly available Federal diff --git a/ckan/model/license.py b/ckan/model/license.py index 7b73e6f9ecb..b9aa6872b20 100644 --- a/ckan/model/license.py +++ b/ckan/model/license.py @@ -1,5 +1,7 @@ from pylons import config import datetime +import urllib2 +from ckan.lib.helpers import json import re class License(object): @@ -41,13 +43,32 @@ class LicenseRegister(object): def __init__(self): group_url = config.get('licenses_group_url', None) if group_url: - from licenses.service import LicensesService2 - self.service = LicensesService2(group_url) - entity_list = self.service.get_licenses() + self.load_licenses(group_url) else: - from licenses import Licenses - entity_list = Licenses().get_group_licenses('ckan_original') - self.licenses = [License(entity) for entity in entity_list] + self._create_license_list(self.default_license_list) + + def load_licenses(self, license_url): + try: + response = urllib2.urlopen(license_url) + response_body = response.read() + except Exception, inst: + msg = "Couldn't connect to licenses service %r: %s" % (license_url, inst) + raise Exception, msg + try: + license_data = json.loads(response_body) + except Exception, inst: + msg = "Couldn't read response from licenses service %r: %s" % (response_body, inst) + raise Exception, inst + self._create_license_list(license_data) + + def _create_license_list(self, license_data): + if isinstance(license_data, dict): + self.licenses = [License(entity) for entity in license_data.values()] + elif isinstance(license_data, list): + self.licenses = [License(entity) for entity in license_data] + else: + msg = "Licenses at %s must be dictionary or list" % license_url + raise ValueError(msg) def __getitem__(self, key, default=Exception): for license in self.licenses: @@ -85,3 +106,207 @@ def get_by_title(self, title, default=None): else: return default + + default_license_list = [ + { + "domain_content": False, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "notspecified", + "is_generic": True, + "is_okd_compliant": False, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "License Not Specified", + "url": "" + }, + { + "domain_content": False, + "domain_data": True, + "domain_software": False, + "family": "", + "id": "odc-pddl", + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Open Data Commons Public Domain Dedication and Licence (PDDL)", + "url": "http://www.opendefinition.org/licenses/odc-pddl" + }, + { + "domain_content": False, + "domain_data": True, + "domain_software": False, + "family": "", + "id": "odc-odbl", + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Open Data Commons Open Database License (ODbL)", + "url": "http://www.opendefinition.org/licenses/odc-odbl" + }, + { + "domain_content": False, + "domain_data": True, + "domain_software": False, + "family": "", + "id": "odc-by", + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Open Data Commons Attribution License", + "url": "http://www.opendefinition.org/licenses/odc-by" + }, + { + "domain_content": True, + "domain_data": True, + "domain_software": False, + "family": "", + "id": "cc-zero", + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Creative Commons CCZero", + "url": "http://www.opendefinition.org/licenses/cc-zero" + }, + { + "domain_content": True, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "cc-by", + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Creative Commons Attribution", + "url": "http://www.opendefinition.org/licenses/cc-by" + }, + { + "domain_content": True, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "cc-by-sa", + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Creative Commons Attribution Share-Alike", + "url": "http://www.opendefinition.org/licenses/cc-by-sa" + }, + { + "domain_content": True, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "gfdl", + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "GNU Free Documentation License", + "url": "http://www.opendefinition.org/licenses/gfdl" + }, + { + "domain_content": True, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "other-open", + "is_generic": True, + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Other (Open)", + "url": "" + }, + { + "domain_content": True, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "other-pd", + "is_generic": True, + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Other (Public Domain)", + "url": "" + }, + { + "domain_content": True, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "other-at", + "is_generic": True, + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Other (Attribution)", + "url": "" + }, + { + "domain_content": True, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "uk-ogl", + "is_okd_compliant": True, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "UK Open Government Licence (OGL)", + "url": "http://reference.data.gov.uk/id/open-government-licence" + }, + { + "domain_content": False, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "cc-nc", + "is_okd_compliant": False, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Creative Commons Non-Commercial (Any)", + "url": "http://creativecommons.org/licenses/by-nc/2.0/" + }, + { + "domain_content": False, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "other-nc", + "is_generic": True, + "is_okd_compliant": False, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Other (Non-Commercial)", + "url": "" + }, + { + "domain_content": False, + "domain_data": False, + "domain_software": False, + "family": "", + "id": "other-closed", + "is_generic": True, + "is_okd_compliant": False, + "is_osi_compliant": False, + "maintainer": "", + "status": "active", + "title": "Other (Not Open)", + "url": "" + } + ] diff --git a/ckan/tests/functional/test_package.py b/ckan/tests/functional/test_package.py index e789b594a5c..d6aceda2c08 100644 --- a/ckan/tests/functional/test_package.py +++ b/ckan/tests/functional/test_package.py @@ -788,7 +788,7 @@ def test_edit_all_fields(self): ) assert len(resources[0]) == 5 notes = u'Very important' - license_id = u'gpl-3.0' + license_id = u'odc-by' state = model.State.ACTIVE tags = (u'tag1', u'tag2', u'tag 3') tags_txt = u','.join(tags) @@ -1152,7 +1152,7 @@ def test_new_all_fields(self): url = u'http://something.com/somewhere.zip' download_url = u'http://something.com/somewhere-else.zip' notes = u'Very important' - license_id = u'gpl-3.0' + license_id = u'odc-by' tags = (u'tag1', u'tag2.', u'tag 3', u'SomeCaps') tags_txt = u','.join(tags) extras = {self.key1:self.value1, 'key2':'value2', 'key3':'value3'} diff --git a/ckan/tests/models/test_license.py b/ckan/tests/models/test_license.py index 142a4939f77..281b6d966d3 100644 --- a/ckan/tests/models/test_license.py +++ b/ckan/tests/models/test_license.py @@ -35,6 +35,5 @@ def test_getitem(self): license = self.licenses[license_id] self.assert_unicode(license.id) self.assert_unicode(license.title) - self.assert_datetime(license.date_created) self.assert_unicode(license.url) diff --git a/ckan/tests/models/test_package.py b/ckan/tests/models/test_package.py index 8371f226f7d..653ea56333b 100644 --- a/ckan/tests/models/test_package.py +++ b/ckan/tests/models/test_package.py @@ -19,7 +19,7 @@ def setup_class(self): self.pkg1 = model.Package(name=self.name) model.Session.add(self.pkg1) self.pkg1.notes = self.notes - self.pkg1.license_id = u'gpl-3.0' + self.pkg1.license_id = u'odc-by' model.Session.commit() model.Session.remove() @@ -71,8 +71,8 @@ def test_create_package(self): package = model.Package.by_name(self.name) assert package.name == self.name assert package.notes == self.notes - assert package.license.id == u'gpl-3.0' - assert package.license.title == u'OSI Approved::GNU General Public License version 3.0 (GPLv3)', package.license.title + assert package.license.id == u'odc-by' + assert package.license.title == u'Open Data Commons Attribution License', package.license.title def test_update_package(self): newnotes = u'Written by Beethoven' diff --git a/requires/lucid_missing.txt b/requires/lucid_missing.txt index a5ed111fdf0..cb49ca034ee 100644 --- a/requires/lucid_missing.txt +++ b/requires/lucid_missing.txt @@ -18,6 +18,5 @@ pairtree==0.7.1-T ofs==0.4.1 apachemiddleware==0.1.1 -licenses==0.6.1 # markupsafe is required by webhelpers==1.2 required by formalchemy with SQLAlchemy 0.6 markupsafe==0.9.2 From 19fc61ac56826407965438bdc5d973e20dc00dc0 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 13 Feb 2012 11:11:58 +0000 Subject: [PATCH 46/59] [#1359,tests][xs]: fix up one test missed in previous commit (gpl -> odc-by for demo license). --- ckan/tests/forms/test_package.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/tests/forms/test_package.py b/ckan/tests/forms/test_package.py index 057c0d2d8e4..06136f77af7 100644 --- a/ckan/tests/forms/test_package.py +++ b/ckan/tests/forms/test_package.py @@ -134,7 +134,7 @@ def test_3_sync_new(self): indict['Package--name'] = u'testname' indict['Package--notes'] = u'some new notes' indict['Package--tags'] = u'russian, tolstoy, ' + newtagname, - indict['Package--license_id'] = u'gpl-3.0' + indict['Package--license_id'] = u'odc-by' indict['Package--extras-newfield0-key'] = u'testkey' indict['Package--extras-newfield0-value'] = u'testvalue' indict['Package--resources-0-url'] = u'http:/1' @@ -185,7 +185,7 @@ def test_4_sync_update(self): indict[prefix + 'name'] = u'annakaren' indict[prefix + 'notes'] = u'new notes' indict[prefix + 'tags'] = u'russian ,' + newtagname - indict[prefix + 'license_id'] = u'gpl-3.0' + indict[prefix + 'license_id'] = u'odc-by' indict[prefix + 'extras-newfield0-key'] = u'testkey' indict[prefix + 'extras-newfield0-value'] = u'testvalue' indict[prefix + 'resources-0-url'] = u'http:/1' From 8b89ced1da1e4a3126cc052dd339c74a090dd194 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 13 Feb 2012 11:21:04 +0000 Subject: [PATCH 47/59] [#1359,doc][xs]: minor updates to docs re license_group_url to reflect new setup. --- ckan/config/deployment.ini_tmpl | 4 ++-- doc/configuration.rst | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ckan/config/deployment.ini_tmpl b/ckan/config/deployment.ini_tmpl index adc875b7d19..85183c1757a 100644 --- a/ckan/config/deployment.ini_tmpl +++ b/ckan/config/deployment.ini_tmpl @@ -52,8 +52,8 @@ who.log_file = %(cache_dir)s/who_log.ini # Location of RDF versions of datasets #rdf_packages = http://semantic.ckan.net/record/ -# Location of licenses group (defaults to local Python licenses dataset) -#licenses_group_url = http://licenses.opendefinition.org/2.0/ckan_original +# Location of licenses group (defaults to cached local version of ckan group) +#licenses_group_url = http://licenses.opendefinition.org/licenses/groups/ckan.json # Dataset form to use package_form = standard diff --git a/doc/configuration.rst b/doc/configuration.rst index 7396b4a38bc..e087f20764b 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -392,17 +392,18 @@ A url pointing to a JSON file containing a list of licence objects. This list determines the licences offered by the system to users, for example when creating or editing a dataset. -This is entirely optional - by default, the system will use the CKAN list of -licences available in the `Python licenses package `_. +This is entirely optional - by default, the system will use an internal cached +version of the CKAN list of licences available from the +http://licenses.opendefinition.org/licenses/groups/ckan.json. -More details about the CKAN license objects - including the licence format and some +More details about the license objects - including the licence format and some example licence lists - can be found at the `Open Licenses Service `_. Examples:: - licenses_group_url = file:///path/to/my/local/json-list-of-licenses.js - licenses_group_url = http://licenses.opendefinition.org/2.0/ckan_original + licenses_group_url = file:///path/to/my/local/json-list-of-licenses.json + licenses_group_url = http://licenses.opendefinition.org/licenses/groups/od.json Messaging Settings From a8b0e3b0cd453c2440e7dff5d82feab62e22057c Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Mon, 13 Feb 2012 11:21:45 +0000 Subject: [PATCH 48/59] [#1359,licenses][xs]: remove unused method get_by_title on LicenseRegister. --- ckan/model/license.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ckan/model/license.py b/ckan/model/license.py index b9aa6872b20..f717113ba92 100644 --- a/ckan/model/license.py +++ b/ckan/model/license.py @@ -97,16 +97,6 @@ def __iter__(self): def __len__(self): return len(self.licenses) - # non-dict like interface - - def get_by_title(self, title, default=None): - for license in self.licenses: - if title == license.title or title == license.title.split('::')[1]: - return license - else: - return default - - default_license_list = [ { "domain_content": False, From e135bb0656d3de8f392d6b9f7be3b062f6ab2d47 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 13 Feb 2012 14:53:07 +0000 Subject: [PATCH 49/59] [1669] Fixes to pass tests in alphabet paging and publisher auth --- ckan/lib/alphabet_paginate.py | 2 +- ckan/logic/auth/publisher/create.py | 4 ++++ ckan/tests/lib/test_alphabet_pagination.py | 4 ++-- ckan/tests/misc/test_auth_profiles.py | 16 +++++++++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/ckan/lib/alphabet_paginate.py b/ckan/lib/alphabet_paginate.py index 75979b3fbad..321c9b6d861 100644 --- a/ckan/lib/alphabet_paginate.py +++ b/ckan/lib/alphabet_paginate.py @@ -45,7 +45,7 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho self.controller_name = controller_name self.available = dict( (c,0,) for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) for c in self.collection: - x = getattr(c, self.alpha_attribute)[0] + x = c if isinstance( c, unicode ) else getattr(c, self.alpha_attribute)[0] self.available[x] = self.available.get(x, 0) + 1 diff --git a/ckan/logic/auth/publisher/create.py b/ckan/logic/auth/publisher/create.py index 7f3e2c407af..c87f8637243 100644 --- a/ckan/logic/auth/publisher/create.py +++ b/ckan/logic/auth/publisher/create.py @@ -104,3 +104,7 @@ def group_create_rest(context, data_dict): return {'success': False, 'msg': _('Valid API key needed to create a group')} return group_create(context, data_dict) + +def activity_create(context, data_dict): + user = context['user'] + return {'success': Authorizer.is_sysadmin(user)} diff --git a/ckan/tests/lib/test_alphabet_pagination.py b/ckan/tests/lib/test_alphabet_pagination.py index 8af1e7f604b..a1bf6ae4749 100644 --- a/ckan/tests/lib/test_alphabet_pagination.py +++ b/ckan/tests/lib/test_alphabet_pagination.py @@ -40,8 +40,8 @@ def test_01_package_page(self): assert pager.startswith('
        '), pager assert 'A' in pager, pager url_base = '/packages' - assert re.search('\B\<\/a\>', pager), pager - assert re.search('\Other\<\/a\>', pager), pager + assert re.search('\B\<\/span\>', pager), pager + assert re.search('\Other\<\/span\>', pager), pager def test_02_package_items(self): diff --git a/ckan/tests/misc/test_auth_profiles.py b/ckan/tests/misc/test_auth_profiles.py index 124cc0317d2..a1d4f59f06f 100644 --- a/ckan/tests/misc/test_auth_profiles.py +++ b/ckan/tests/misc/test_auth_profiles.py @@ -34,6 +34,11 @@ def test_authorizer_count(self): 'ckan.logic.auth.publisher': 0 } + module_items = { + 'ckan.logic.auth': [], + 'ckan.logic.auth.publisher': [] + } + for module_root in modules.keys(): for auth_module_name in ['get', 'create', 'update','delete']: module_path = '%s.%s' % (module_root, auth_module_name,) @@ -45,6 +50,15 @@ def test_authorizer_count(self): for key, v in module.__dict__.items(): if not key.startswith('_'): modules[module_root] = modules[module_root] + 1 + module_items[module_root].append( key ) - assert modules['ckan.logic.auth'] == modules['ckan.logic.auth.publisher'], modules + err = [] + if modules['ckan.logic.auth'] != modules['ckan.logic.auth.publisher']: + oldauth = module_items['ckan.logic.auth'] + pubauth = module_items['ckan.logic.auth.publisher'] + for e in [n for n in oldauth if not n in pubauth]: + err.append( '%s is in auth but not publisher auth ' % e ) + for e in [n for n in pubauth if not n in oldauth]: + err.append( '%s is in publisher auth but not auth ' % e ) + assert modules['ckan.logic.auth'] == modules['ckan.logic.auth.publisher'], err From 3d7cbf02f0eb2130c13a67e8a66259701b40cfe5 Mon Sep 17 00:00:00 2001 From: Toby Date: Mon, 13 Feb 2012 16:26:46 +0000 Subject: [PATCH 50/59] [#1057][xs] escape pjson callback --- ckan/controllers/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ckan/controllers/api.py b/ckan/controllers/api.py index 3689020a4c5..7e6c12ac6a0 100644 --- a/ckan/controllers/api.py +++ b/ckan/controllers/api.py @@ -1,4 +1,5 @@ import logging +import cgi from paste.util.multidict import MultiDict from webob.multidict import UnicodeMultiDict @@ -70,7 +71,8 @@ def _finish(self, status_int, response_data=None, if status_int==200 and request.params.has_key('callback') and \ (request.method == 'GET' or \ c.logic_function and request.method == 'POST'): - callback = request.params['callback'] + # escape callback to remove '<', '&', '>' chars + callback = cgi.escape(request.params['callback']) response_msg = self._wrap_jsonp(callback, response_msg) return response_msg From 9d808a98debd046928031e930167c1fd3bc2d2d2 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Mon, 13 Feb 2012 16:58:19 +0000 Subject: [PATCH 51/59] [xs] Minor tweak to how group members are accessed by type in Group model --- ckan/model/group.py | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/ckan/model/group.py b/ckan/model/group.py index 6636236f921..89eb7d513e7 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -21,7 +21,7 @@ Column('capacity', UnicodeText, nullable=False), Column('group_id', UnicodeText, ForeignKey('group.id')), ) - + vdm.sqlalchemy.make_table_stateful(member_table) member_revision_table = make_revisioned_table(member_table) @@ -50,7 +50,7 @@ def __init__(self, group=None, table_id=None, group_id=None, self.table_name = table_name self.capacity = capacity self.state = state - + @classmethod def get(cls, reference): '''Returns a group object referenced by its id or name.''' @@ -59,17 +59,17 @@ def get(cls, reference): if member == None: member = cls.by_name(reference) return member - - + + def get_related(self, type): """ TODO: Determine if this is useful Get all objects that are members of the group of the specified type. - - Should the type be used to get table_name or should we use the one in + + Should the type be used to get table_name or should we use the one in the constructor """ pass - + def related_packages(self): # TODO do we want to return all related packages or certain ones? return Session.query(Package).filter_by(id=self.table_id).all() @@ -77,8 +77,8 @@ def related_packages(self): class Group(vdm.sqlalchemy.RevisionedObjectMixin, vdm.sqlalchemy.StatefulObjectMixin, DomainObject): - - def __init__(self, name=u'', title=u'', description=u'', + + def __init__(self, name=u'', title=u'', description=u'', type=u'group', approval_status=u'approved' ): self.name = name self.title = title @@ -106,8 +106,8 @@ def get(cls, reference): def set_approval_status(self, status): """ Aproval status can be set on a group, where currently it does - nothing other than act as an indication of whether it was - approved or not. It may be that we want to tie the object + nothing other than act as an indication of whether it was + approved or not. It may be that we want to tie the object status to the approval status """ assert status in ["approved", "pending", "denied"] @@ -115,19 +115,24 @@ def set_approval_status(self, status): if status == "denied": pass - def members_of_type(self, object_type): + def members_of_type(self, object_type, capacity=None): object_type_string = object_type.__name__.lower() query = Session.query(object_type).\ filter_by(state=vdm.sqlalchemy.State.ACTIVE).\ filter(group_table.c.id == self.id).\ filter(member_table.c.state == 'active').\ - filter(member_table.c.table_name == object_type_string).\ - join(member_table, member_table.c.table_id == getattr(object_type,'id') ).\ + filter(member_table.c.table_name == object_type_string) + + if capacity: + query = query.filter(member_table.c.capacity == capacity) + + query = query.join(member_table, member_table.c.table_id == getattr(object_type,'id') ).\ join(group_table, group_table.c.id == member_table.c.group_id) + return query def add_child(self, object_instance): - object_type_string = object_instance.__class__.__name__.lower() + object_type_string = object_instance.__class__.__name__.lower() if not object_instance in self.members_of_type(object_instance.__class__).all(): member = Member(group=self, table_id=getattr(object_instance,'id'), table_name=object_type_string) Session.add(member) @@ -149,7 +154,7 @@ def search_by_name(cls, text_query): def as_dict(self, ref_package_by='name'): _dict = DomainObject.as_dict(self) - _dict['packages'] = [getattr(package, ref_package_by) for package in self.packages] + _dict['packages'] = [getattr(package, ref_package_by) for package in self.packages] _dict['extras'] = dict([(key, value) for key, value in self.extras.items()]) if ( self.type == 'publisher' ): _dict['users'] = [getattr(user, "name") for user in self.members_of_type(User)] @@ -175,9 +180,9 @@ def get_groups(self, group_type=None, capacity=None): filter(model.Member.table_id == self.id).all() groups = self._groups - if group_type: + if group_type: groups = [g for g in groups if g.type == group_type] - if capacity: + if capacity: groups = [g for g in groups if g.capacity == capacity] return groups @@ -210,7 +215,7 @@ def __repr__(self): return '' % self.name -mapper(Group, group_table, +mapper(Group, group_table, extension=[vdm.sqlalchemy.Revisioner(group_revision_table),], ) From dc5ca52ab053b7d214dc946ccc229990abb91047 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 14 Feb 2012 09:49:58 +0000 Subject: [PATCH 52/59] [xs] Fix the query to fetch group members of a particular model type --- ckan/model/group.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ckan/model/group.py b/ckan/model/group.py index 89eb7d513e7..def774fe164 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -118,7 +118,6 @@ def set_approval_status(self, status): def members_of_type(self, object_type, capacity=None): object_type_string = object_type.__name__.lower() query = Session.query(object_type).\ - filter_by(state=vdm.sqlalchemy.State.ACTIVE).\ filter(group_table.c.id == self.id).\ filter(member_table.c.state == 'active').\ filter(member_table.c.table_name == object_type_string) From 45ac98451800b9be45cfc25a02c48ab20808251c Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 14 Feb 2012 10:09:37 +0000 Subject: [PATCH 53/59] [1794] Reducing the amount of user clutter --- .../publisher_form/templates/publisher_form.html | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/ckanext/publisher_form/templates/publisher_form.html b/ckanext/publisher_form/templates/publisher_form.html index 45cb350b6fb..f83c151990c 100644 --- a/ckanext/publisher_form/templates/publisher_form.html +++ b/ckanext/publisher_form/templates/publisher_form.html @@ -1,5 +1,5 @@ -
        Errors in form
        - +

        Extras

        @@ -80,7 +80,7 @@

        Extras

        Users

        -
        +
        @@ -89,7 +89,7 @@

        Users

        Admin - Editor + Editor
        @@ -99,10 +99,7 @@

        Users

        Add users

        -
        - Admin - Editor -
        +
        From db71e3072786a6718ecbe28130d89165aa96513e Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 14 Feb 2012 10:34:13 +0000 Subject: [PATCH 54/59] [xs] Fix for alpha pagination when using strings rather than objects --- ckan/lib/alphabet_paginate.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/ckan/lib/alphabet_paginate.py b/ckan/lib/alphabet_paginate.py index 321c9b6d861..f30f5f9c78d 100644 --- a/ckan/lib/alphabet_paginate.py +++ b/ckan/lib/alphabet_paginate.py @@ -7,7 +7,7 @@ collection=query, page=request.params.get('page', 'A'), ) - Template: + Template: ${c.page.pager()} ${package_list(c.page.items)} ${c.page.pager()} @@ -21,7 +21,7 @@ from routes import url_for class AlphaPage(object): - def __init__(self, collection, alpha_attribute, page, other_text, paging_threshold=50, + def __init__(self, collection, alpha_attribute, page, other_text, paging_threshold=50, controller_name='tag'): ''' @param collection - sqlalchemy query of all the items to paginate @@ -34,7 +34,7 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho start paginating them. @param controller_name - The name of the controller that will be linked to, which defaults to tag. The controller name should be the - same as the route so for some this will be the full + same as the route so for some this will be the full controller name such as 'A.B.controllers.C:ClassName' ''' self.collection = collection @@ -45,10 +45,10 @@ def __init__(self, collection, alpha_attribute, page, other_text, paging_thresho self.controller_name = controller_name self.available = dict( (c,0,) for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ) for c in self.collection: - x = c if isinstance( c, unicode ) else getattr(c, self.alpha_attribute)[0] + x = c[0] if isinstance( c, unicode ) else getattr(c, self.alpha_attribute)[0] self.available[x] = self.available.get(x, 0) + 1 - - + + def pager(self, q=None): '''Returns pager html - for navigating between the pages. @@ -72,10 +72,10 @@ def pager(self, q=None): if self.available.get(letter, 0): page_element = HTML.a(class_='pager_link', href=url_for(controller=self.controller_name, action='index', page=letter),c=letter) else: - page_element = HTML.span(class_="pager_empty", c=letter) + page_element = HTML.span(class_="pager_empty", c=letter) else: page_element = HTML.span(class_='pager_curpage', c=letter) - pages.append(page_element) + pages.append(page_element) div = HTML.tag('div', class_='pager', *pages) return div @@ -89,7 +89,7 @@ def items(self): attribute = getattr(query.table.c, self.alpha_attribute) elif sqav.startswith("0.5"): - attribute = getattr(query._entity_zero().selectable.c, + attribute = getattr(query._entity_zero().selectable.c, self.alpha_attribute) else: entity = getattr(query.column_descriptions[0]['expr'], @@ -111,13 +111,15 @@ def items(self): if self.item_count >= self.paging_threshold: if self.page != self.other_text: if isinstance(self.collection[0], dict): - items = [x for x in self.collection if x[self.alpha_attribute][0:1].lower() == self.page.lower()] + items = [x for x in self.collection if x[self.alpha_attribute][0:1].lower() == self.page.lower()] + elif isinstance(self.collection[0], unicode): + items = [x for x in self.collection if x[0:1].lower() == self.page.lower()] else: items = [x for x in self.collection if getattr(x,self.alpha_attribute)[0:1].lower() == self.page.lower()] else: # regexp search if isinstance(self.collection[0], dict): - items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x[self.alpha_attribute])] + items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x[self.alpha_attribute])] else: items = [x for x in self.collection if re.match('^[^a-zA-Z].*',x)] items.sort() From 7f029ce19b619b103104a646d84b52dbf9ca8f88 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 14 Feb 2012 12:15:49 +0000 Subject: [PATCH 55/59] Code (whitespace) cleanup --- ckan/lib/helpers.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 80926f47e17..270f9c90446 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -33,7 +33,7 @@ import json except ImportError: import simplejson as json - + class Message(object): """A message returned by ``Flash.pop_messages()``. @@ -56,16 +56,16 @@ def __str__(self): __unicode__ = __str__ def __html__(self): - if self.allow_html: + if self.allow_html: return self.message else: return escape(self.message) class _Flash(object): - + # List of allowed categories. If None, allow any category. categories = ["warning", "notice", "error", "success"] - + # Default category if none is specified. default_category = "notice" @@ -112,13 +112,13 @@ def are_there_messages(self): _flash = _Flash() -def flash_notice(message, allow_html=False): +def flash_notice(message, allow_html=False): _flash(message, category='notice', allow_html=allow_html) -def flash_error(message, allow_html=False): +def flash_error(message, allow_html=False): _flash(message, category='error', allow_html=allow_html) -def flash_success(message, allow_html=False): +def flash_success(message, allow_html=False): _flash(message, category='success', allow_html=allow_html) def are_there_flash_messages(): @@ -126,12 +126,12 @@ def are_there_flash_messages(): # FIXME: shouldn't have to pass the c object in to this. def nav_link(c, text, controller, **kwargs): - highlight_actions = kwargs.pop("highlight_actions", + highlight_actions = kwargs.pop("highlight_actions", kwargs["action"]).split() return link_to( text, url_for(controller=controller, **kwargs), - class_=('active' if + class_=('active' if c.controller == controller and c.action in highlight_actions else '') ) @@ -139,22 +139,22 @@ def nav_link(c, text, controller, **kwargs): # FIXME: shouldn't have to pass the c object in to this. def subnav_link(c, text, action, **kwargs): return link_to( - text, + text, url_for(action=action, **kwargs), class_=('active' if c.action == action else '') ) - + def subnav_named_route(c, text, routename,**kwargs): """ Generate a subnav element based on a named route """ return link_to( - text, + text, url_for(str(routename), **kwargs), class_=('active' if c.action == kwargs['action'] else '') - ) + ) def facet_items(c, name, limit=10): from pylons import request - if not c.facets or not c.facets.get(name): + if not c.facets or not c.facets.get(name): return [] facets = [] for k, v in c.facets.get(name).items(): @@ -165,7 +165,7 @@ def facet_items(c, name, limit=10): return sorted(facets, key=lambda (k, v): v, reverse=True)[:limit] def facet_title(name): - from pylons import config + from pylons import config return config.get('search.facets.%s.title' % name, name.capitalize()) def am_authorized(c, action, domain_object=None): @@ -208,7 +208,7 @@ def linked_user(user, maxlength=0): displayname = user.display_name if maxlength and len(user.display_name) > maxlength: displayname = displayname[:maxlength] + '...' - return _icon + link_to(displayname, + return _icon + link_to(displayname, url_for(controller='user', action='read', id=_name)) def linked_authorization_group(authgroup, maxlength=0): @@ -223,7 +223,7 @@ def linked_authorization_group(authgroup, maxlength=0): displayname = authgroup.name or authgroup.id if maxlength and len(display_name) > maxlength: displayname = displayname[:maxlength] + '...' - return link_to(displayname, + return link_to(displayname, url_for(controller='authorization_group', action='read', id=displayname)) def group_name_to_title(name): @@ -286,7 +286,7 @@ def render_datetime(datetime_, date_format=None, with_hours=False): (Y-m-d H:m). If timestamp is badly formatted, then a blank string is returned. ''' - if not date_format: + if not date_format: date_format = '%b %d, %Y' if with_hours: date_format += ', %H:%M' From 643a800bfa7b76074203f332218506d1db3da854 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 14 Feb 2012 13:15:12 +0000 Subject: [PATCH 56/59] [1794] Added autocomplete for groups --- ckan/config/routing.py | 41 ++++++++++--------- ckan/controllers/api.py | 65 ++++++++++++++++++++---------- ckan/model/group.py | 8 +++- ckan/public/scripts/application.js | 60 +++++++++++++++++---------- 4 files changed, 109 insertions(+), 65 deletions(-) diff --git a/ckan/config/routing.py b/ckan/config/routing.py index fdb44386038..c65999cb603 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -14,11 +14,11 @@ def make_map(): """Create, configure and return the routes Mapper""" - # import controllers here rather than at root level because + # import controllers here rather than at root level because # pylons config is initialised by this point. from ckan.controllers.package import register_pluggable_behaviour as register_package_behaviour from ckan.controllers.group import register_pluggable_behaviour as register_group_behaviour - + map = Mapper(directory=config['pylons.paths']['controllers'], always_scan=config['debug']) map.minimization = False @@ -116,7 +116,7 @@ def make_map(): map.connect('/api/search/{register}', controller='api', action='search') map.connect('/api/tag_counts', controller='api', action='tag_counts') - + map.connect('/api/rest', controller='api', action='index') map.connect('/api/action/{logic_function}', controller='api', action='action', @@ -182,6 +182,7 @@ def make_map(): map.connect('/api/2/util/authorizationgroup/autocomplete', controller='api', action='authorizationgroup_autocomplete') + map.connect('/api/2/util/group/autocomplete', controller='api', action='group_autocomplete') map.connect('/api/util/markdown', controller='api', action='markdown') map.connect('/api/util/dataset/munge_name', controller='api', action='munge_package_name') @@ -192,7 +193,7 @@ def make_map(): ########### ## /END API ########### - + map.redirect("/packages", "/dataset") map.redirect("/packages/{url:.*}", "/dataset/{url}") map.redirect("/package", "/dataset") @@ -231,7 +232,7 @@ def make_map(): ])) ) map.connect('/dataset/{id}', controller='package', action='read') - map.connect('/dataset/{id}/resource/{resource_id}', + map.connect('/dataset/{id}/resource/{resource_id}', controller='package', action="resource_read" ) @@ -243,11 +244,11 @@ def make_map(): ##map.connect('/group/new', controller='group_formalchemy', action='new') ##map.connect('/group/edit/{id}', controller='group_formalchemy', action='edit') - # These named routes are used for custom group forms which will use the + # These named routes are used for custom group forms which will use the # names below based on the group.type (dataset_group is the default type) map.connect('group_index', '/group', controller='group', action='index') map.connect('group_list', '/group/list', controller='group', action='list') - map.connect('group_new', '/group/new', controller='group', action='new') + map.connect('group_new', '/group/new', controller='group', action='new') map.connect('group_action', '/group/{action}/{id}', controller='group', requirements=dict(action='|'.join([ 'edit', @@ -258,9 +259,9 @@ def make_map(): map.connect('group_read', '/group/{id}', controller='group', action='read') register_package_behaviour(map) - register_group_behaviour(map) + register_group_behaviour(map) + - # authz group map.redirect("/authorizationgroups", "/authorizationgroup") map.redirect("/authorizationgroups/{url:.*}", "/authorizationgroup/{url}") @@ -301,17 +302,17 @@ def make_map(): map.connect('ckanadmin_index', '/ckan-admin', controller='admin', action='index') map.connect('ckanadmin', '/ckan-admin/{action}', controller='admin') - + # Storage routes - map.connect('storage_api', "/api/storage", - controller='ckan.controllers.storage:StorageAPIController', + map.connect('storage_api', "/api/storage", + controller='ckan.controllers.storage:StorageAPIController', action='index') - map.connect('storage_api_set_metadata', '/api/storage/metadata/{label:.*}', - controller='ckan.controllers.storage:StorageAPIController', + map.connect('storage_api_set_metadata', '/api/storage/metadata/{label:.*}', + controller='ckan.controllers.storage:StorageAPIController', action='set_metadata', conditions={'method': ['PUT','POST']}) - map.connect('storage_api_get_metadata', '/api/storage/metadata/{label:.*}', - controller='ckan.controllers.storage:StorageAPIController', + map.connect('storage_api_get_metadata', '/api/storage/metadata/{label:.*}', + controller='ckan.controllers.storage:StorageAPIController', action='get_metadata', conditions={'method': ['GET']}) map.connect('storage_api_auth_request', @@ -337,12 +338,12 @@ def make_map(): map.connect('storage_file', '/storage/f/{label:.*}', controller='ckan.controllers.storage:StorageController', action='file') - - + + for plugin in routing_plugins: map = plugin.after_map(map) - - + + map.redirect('/*(url)/', '/{url}', _redirect_code='301 Moved Permanently') map.connect('/*url', controller='template', action='view') diff --git a/ckan/controllers/api.py b/ckan/controllers/api.py index 7e6c12ac6a0..dccc85ca3ef 100644 --- a/ckan/controllers/api.py +++ b/ckan/controllers/api.py @@ -1,7 +1,7 @@ import logging import cgi -from paste.util.multidict import MultiDict +from paste.util.multidict import MultiDict from webob.multidict import UnicodeMultiDict from ckan.lib.base import BaseController, response, c, _, gettext, request @@ -133,15 +133,15 @@ def _set_response_header(self, name, value): def get_api(self, ver=None): response_data = {} response_data['version'] = ver or '1' - return self._finish_ok(response_data) - + return self._finish_ok(response_data) + def action(self, logic_function): function = get_action(logic_function) if not function: log.error('Can\'t find logic function: %s' % logic_function) return self._finish_bad_request( gettext('Action name not known: %s') % str(logic_function)) - + context = {'model': model, 'session': model.Session, 'user': c.user} model.Session()._context = context return_dict = {'help': function.__doc__} @@ -180,7 +180,7 @@ def action(self, logic_function): return_dict['success'] = False return self._finish(404, return_dict, content_type='json') except ValidationError, e: - error_dict = e.error_dict + error_dict = e.error_dict error_dict['__type'] = 'Validation Error' return_dict['error'] = error_dict return_dict['success'] = False @@ -192,17 +192,17 @@ def action(self, logic_function): (_('Parameter Error'), e.extra_msg)} return_dict['success'] = False log.error('Parameter error: %r' % e.extra_msg) - return self._finish(409, return_dict, content_type='json') + return self._finish(409, return_dict, content_type='json') except SearchQueryError, e: return_dict['error'] = {'__type': 'Search Query Error', 'message': 'Search Query is invalid: %r' % e.args } return_dict['success'] = False - return self._finish(400, return_dict, content_type='json') + return self._finish(400, return_dict, content_type='json') except SearchError, e: return_dict['error'] = {'__type': 'Search Error', 'message': 'Search error: %r' % e.args } return_dict['success'] = False - return self._finish(409, return_dict, content_type='json') + return self._finish(409, return_dict, content_type='json') return self._finish_ok(return_dict) def list(self, ver=None, register=None, subregister=None, id=None): @@ -227,7 +227,7 @@ def list(self, ver=None, register=None, subregister=None, id=None): ('activity', 'details'): get_action('activity_detail_list') } - action = action_map.get((register, subregister)) + action = action_map.get((register, subregister)) if not action: action = action_map.get(register) if not action: @@ -261,14 +261,14 @@ def show(self, ver=None, register=None, subregister=None, id=None, id2=None): action_map[('package', type)] = get_action('package_relationships_list') log.debug('show: %s' % context) - action = action_map.get((register, subregister)) + action = action_map.get((register, subregister)) if not action: action = action_map.get(register) if not action: return self._finish_bad_request( gettext('Cannot read entity of this type: %s') % register) try: - + return self._finish_ok(action(context, data_dict)) except NotFound, e: extra_msg = e.extra_msg @@ -305,7 +305,7 @@ def create(self, ver=None, register=None, subregister=None, id=None, id2=None): return self._finish_bad_request( gettext('JSON Error: %s') % str(inst)) - action = action_map.get((register, subregister)) + action = action_map.get((register, subregister)) if not action: action = action_map.get(register) if not action: @@ -340,7 +340,7 @@ def create(self, ver=None, register=None, subregister=None, id=None, id2=None): except: model.Session.rollback() raise - + def update(self, ver=None, register=None, subregister=None, id=None, id2=None): action_map = { ('dataset', 'relationships'): get_action('package_relationship_update_rest'), @@ -363,7 +363,7 @@ def update(self, ver=None, register=None, subregister=None, id=None, id2=None): except ValueError, inst: return self._finish_bad_request( gettext('JSON Error: %s') % str(inst)) - action = action_map.get((register, subregister)) + action = action_map.get((register, subregister)) if not action: action = action_map.get(register) if not action: @@ -409,7 +409,7 @@ def delete(self, ver=None, register=None, subregister=None, id=None, id2=None): log.debug('delete %s/%s/%s/%s' % (register, id, subregister, id2)) - action = action_map.get((register, subregister)) + action = action_map.get((register, subregister)) if not action: action = action_map.get(register) if not action: @@ -429,7 +429,7 @@ def delete(self, ver=None, register=None, subregister=None, id=None, id2=None): return self._finish(409, e.error_dict, content_type='json') def search(self, ver=None, register=None): - + log.debug('search %s params: %r' % (register, request.params)) ver = ver or '1' # i.e. default to v1 if register == 'revision': @@ -468,7 +468,7 @@ def search(self, ver=None, register=None): params['fl'] = 'id' if ver == '2' else 'name' try: - if register == 'resource': + if register == 'resource': query = query_for(model.Resource) # resource search still uses ckan query parser @@ -528,7 +528,7 @@ def _get_search_params(cls, request_params): params = request_params if not isinstance(params, (UnicodeMultiDict, dict)): raise ValueError, _('Request params must be in form of a json encoded dictionary.') - return params + return params def markdown(self, ver=None): raw_markdown = request.params.get('q', '') @@ -584,6 +584,27 @@ def user_autocomplete(self): user_list = get_action('user_autocomplete')(context,data_dict) return user_list + @jsonpify + def group_autocomplete(self): + q = request.params.get('q', '') + t = request.params.get('type', None) + limit = request.params.get('limit', 20) + try: + limit = int(limit) + except: + limit = 20 + limit = min(50, limit) + + query = model.Group.search_by_name(q, t) + def convert_to_dict(user): + out = {} + for k in ['id', 'name', 'title']: + out[k] = getattr(user, k) + return out + query = query.limit(limit) + out = map(convert_to_dict, query.all()) + return out + @jsonpify def authorizationgroup_autocomplete(self): @@ -594,7 +615,7 @@ def authorizationgroup_autocomplete(self): except: limit = 20 limit = min(50, limit) - + query = model.AuthorizationGroup.search(q) def convert_to_dict(user): out = {} @@ -615,7 +636,7 @@ def is_slug_valid(self): response_data = dict(valid=not bool(group_exists(slug))) return self._finish_ok(response_data) return self._finish_bad_request(gettext('Bad slug type: %s') % slugtype) - + def dataset_autocomplete(self): q = request.params.get('incomplete', '') @@ -677,8 +698,8 @@ def munge_package_name(self): def munge_title_to_package_name(self): name = request.params.get('title') or request.params.get('name') munged_name = munge_title_to_name(name) - return self._finish_ok(munged_name) - + return self._finish_ok(munged_name) + def munge_tag(self): tag = request.params.get('tag') or request.params.get('name') munged_tag = munge_tag(tag) diff --git a/ckan/model/group.py b/ckan/model/group.py index def774fe164..e90b35a4c35 100644 --- a/ckan/model/group.py +++ b/ckan/model/group.py @@ -147,9 +147,13 @@ def active_packages(self, load_eager=True): return query @classmethod - def search_by_name(cls, text_query): + def search_by_name(cls, text_query, group_type=None): text_query = text_query.strip().lower() - return Session.query(cls).filter(cls.name.contains(text_query)) + if not group_type: + q = Session.query(cls).filter(cls.name.contains(text_query)) + else: + q = Session.query(cls).filter(cls.name.contains(text_query)).filter(cls.type==group_type) + return q.order_by(cls.title) def as_dict(self, ref_package_by='name'): _dict = DomainObject.as_dict(self) diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js index 61c03ec5f67..dd1c20424da 100644 --- a/ckan/public/scripts/application.js +++ b/ckan/public/scripts/application.js @@ -1,7 +1,8 @@ (function ($) { $(document).ready(function () { CKAN.Utils.setupUserAutocomplete($('input.autocomplete-user')); - CKAN.Utils.setupPublisherUserAutocomplete($('input.autocomplete-publisher-user')); + CKAN.Utils.setupPublisherUserAutocomplete($('input.autocomplete-publisher-user')); + CKAN.Utils.setupGroupAutocomplete($('input.autocomplete-group')); CKAN.Utils.setupAuthzGroupAutocomplete($('input.autocomplete-authzgroup')); CKAN.Utils.setupPackageAutocomplete($('input.autocomplete-dataset')); CKAN.Utils.setupTagAutocomplete($('input.autocomplete-tag')); @@ -61,7 +62,7 @@ e.preventDefault(); window.location = ($(e.target).attr('action')); }); - + var isDatasetEdit = $('body.package.edit').length > 0; if (isDatasetEdit) { CKAN.Utils.setupUrlEditor('package',readOnly=true); @@ -164,10 +165,10 @@ CKAN.Utils = function($, my) { var titleChanged = function() { var lastTitle = ""; - var regexToHyphen = [ new RegExp('[ .:/_]', 'g'), - new RegExp('[^a-zA-Z0-9-_]', 'g'), + var regexToHyphen = [ new RegExp('[ .:/_]', 'g'), + new RegExp('[^a-zA-Z0-9-_]', 'g'), new RegExp('-+', 'g')]; - var regexToDelete = [ new RegExp('^-*', 'g'), + var regexToDelete = [ new RegExp('^-*', 'g'), new RegExp('-*$', 'g')]; var titleToSlug = function(title) { @@ -393,11 +394,11 @@ CKAN.Utils = function($, my) { var new_name = split[1] + '__' + (parseInt(split[2]) + 1) + '__' + split[3] input_box.attr('name', new_name) input_box.attr('id', new_name) - + var capacity = $("input:radio[name=add-user-capacity]:checked").val(); parent_dd.before( '' + - '' + + '' + '
        ' + ui.item.label + '
        ' ); @@ -450,6 +451,23 @@ CKAN.Utils = function($, my) { }); }; + my.setupGroupAutocomplete = function(elements) { + elements.autocomplete({ + minLength: 2, + source: function(request, callback) { + var url = CKAN.SITE_URL + '/api/2/util/group/autocomplete?q=' + request.term; + $.getJSON(url, function(data) { + $.each(data, function(idx, userobj) { + var label = userobj.name; + userobj.label = label; + userobj.value = userobj.name; + }); + callback(data); + }); + } + }); + }; + my.setupMarkdownEditor = function(markdownEditor) { // Markdown editor hooks markdownEditor.find('button, div.markdown-preview').live('click', function(e) { @@ -508,7 +526,7 @@ CKAN.Utils = function($, my) { } }; - // Show/hide fieldset sections from the edit dataset form. + // Show/hide fieldset sections from the edit dataset form. my.setupDatasetEditNavigation = function() { function showSection(sectionToShowId) { @@ -523,7 +541,7 @@ CKAN.Utils = function($, my) { // Prefix="#section-" var initialSection = window.location.hash.slice(9) || 'basic-information'; showSection(initialSection); - + // Adjust form state on click $('.dataset-edit-nav li a').live('click', function(e) { var $el = $(e.target); @@ -531,7 +549,7 @@ CKAN.Utils = function($, my) { var showMe = $el.attr('href').slice(9); showSection(showMe); return false; - }); + }); }; return my; @@ -554,8 +572,8 @@ CKAN.View.DatasetEditForm = Backbone.View.extend({ messageDiv.show(200); boundToUnload = true; - window.onbeforeunload = function () { - return CKAN.Strings.youHaveUnsavedChanges; + window.onbeforeunload = function () { + return CKAN.Strings.youHaveUnsavedChanges; }; } } @@ -621,7 +639,7 @@ CKAN.View.ResourceEditList = Backbone.View.extend({ // Create a row from the template var $tr = $(''); $tr.html($.tmpl( - CKAN.Templates.resourceEntry, + CKAN.Templates.resourceEntry, { resource: resource.toTemplateJSON(), num: position, resourceTypeOptions: [ @@ -651,9 +669,9 @@ CKAN.View.ResourceEditList = Backbone.View.extend({ expandedTable.animate( {height:0}, animTime, - function() { + function() { expandedTable.height(finalHeight); - expandedTable.hide(); + expandedTable.hide(); } ); } @@ -681,7 +699,7 @@ CKAN.View.ResourceEditList = Backbone.View.extend({ }; // == Inner Functions: Update the name as you type == // - var setName = function(newName) { + var setName = function(newName) { $link = $tr.find('.js-resource-edit-toggle'); newName = newName || (''+CKAN.Strings.noNameBrackets+''); // Need to structurally modify the DOM to force a re-render of text @@ -739,10 +757,10 @@ CKAN.View.ResourceAddTabs = Backbone.View.extend({ clickButton: function(e) { e.preventDefault(); var $target = $(e.target); - + if ($target.is('.depressed')) { this.reset(); - } + } else { this.reset(); $target.addClass('depressed'); @@ -900,7 +918,7 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ my.showPlainTextData(data); }); } - else if (resourceData.formatNormalized in {'html':'', 'htm':''} + else if (resourceData.formatNormalized in {'html':'', 'htm':''} || resourceData.url.substring(0,23)=='http://docs.google.com/') { // we displays a fullscreen dialog with the url in an iframe. my.$dialog.empty(); @@ -911,7 +929,7 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ my.$dialog.append(el); } // images - else if (resourceData.formatNormalized in {'png':'', 'jpg':'', 'gif':''} + else if (resourceData.formatNormalized in {'png':'', 'jpg':'', 'gif':''} || resourceData.resource_type=='image') { // we displays a fullscreen dialog with the url in an iframe. my.$dialog.empty(); @@ -922,7 +940,7 @@ CKAN.View.ResourceAddLink = Backbone.View.extend({ my.$dialog.append(el); } else { - // Cannot reliably preview this item - with no mimetype/format information, + // Cannot reliably preview this item - with no mimetype/format information, // can't guarantee it's not a remote binary file such as an executable. my.showError({ title: 'Preview not available for data type: ' + resourceData.formatNormalized From 0546a5ed1be7f090d1eb3f8ebeb2071b2f324c10 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 14 Feb 2012 15:16:36 +0000 Subject: [PATCH 57/59] [1794] Allowing group create/update to optionally add the parent group --- ckan/controllers/group.py | 70 +++++++++++++++++++------------------ ckan/logic/action/create.py | 29 +++++++++------ ckan/logic/action/update.py | 46 +++++++++++++++--------- 3 files changed, 84 insertions(+), 61 deletions(-) diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 02f8bc306be..b962a8d7c99 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -37,7 +37,7 @@ def register_pluggable_behaviour(map): exception will be raised. """ global _default_controller_behaviour - + # Create the mappings and register the fallback behaviour if one is found. for plugin in PluginImplementations(IGroupForm): if plugin.is_fallback(): @@ -48,24 +48,24 @@ def register_pluggable_behaviour(map): for group_type in plugin.group_types(): # Create the routes based on group_type here, this will allow us to have top level - # objects that are actually Groups, but first we need to make sure we are not + # objects that are actually Groups, but first we need to make sure we are not # clobbering an existing domain - + # Our version of routes doesn't allow the environ to be passed into the match call # and so we have to set it on the map instead. This looks like a threading problem # waiting to happen but it is executed sequentially from instead the routing setup - map.connect('%s_index' % (group_type,), - '/%s' % (group_type,), controller='group', action='index') - map.connect('%s_new' % (group_type,), - '/%s/new' % (group_type,), controller='group', action='new') - map.connect('%s_read' % (group_type,), - '/%s/{id}' % (group_type,), controller='group', action='read') + map.connect('%s_index' % (group_type,), + '/%s' % (group_type,), controller='group', action='index') + map.connect('%s_new' % (group_type,), + '/%s/new' % (group_type,), controller='group', action='new') + map.connect('%s_read' % (group_type,), + '/%s/{id}' % (group_type,), controller='group', action='read') map.connect('%s_action' % (group_type,), '/%s/{action}/{id}' % (group_type,), controller='group', requirements=dict(action='|'.join(['edit', 'authz', 'history' ])) - ) - + ) + if group_type in _controller_behaviour_for: raise ValueError, "An existing IGroupForm is "\ "already associated with the package type "\ @@ -105,8 +105,8 @@ class DefaultGroupForm(object): Note - this isn't a plugin implementation. This is deliberate, as we don't want this being registered. """ - - def group_form(self): + + def group_form(self): return 'group/new_group_form.html' def form_to_db_schema(self): @@ -141,7 +141,7 @@ def check_data_dict(self, data_dict): def setup_template_variables(self, context, data_dict): c.is_sysadmin = Authorizer().is_sysadmin(c.user) - + ## This is messy as auths take domain object not data_dict context_group = context.get('group',None) group = context_group or c.group @@ -154,16 +154,16 @@ def setup_template_variables(self, context, data_dict): except NotAuthorized: c.auth_for_change_state = False -############## End of pluggable group behaviour ############## +############## End of pluggable group behaviour ############## class GroupController(BaseController): - ## hooks for subclasses + ## hooks for subclasses def _group_form(self, group_type=None): return _lookup_plugin(group_type).group_form() - + def _form_to_db_schema(self, group_type=None): return _lookup_plugin(group_type).form_to_db_schema() @@ -188,7 +188,7 @@ def index(self): check_access('site_read', context) except NotAuthorized: abort(401, _('Not authorized to see this page')) - + results = get_action('group_list')(context, data_dict) c.page = Page( @@ -202,7 +202,7 @@ def index(self): def read(self, id): from ckan.lib.search import SearchError - group_type = self._get_group_type(id.split('@')[0]) + group_type = self._get_group_type(id.split('@')[0]) context = {'model': model, 'session': model.Session, 'user': c.user or c.author, 'schema': self._form_to_db_schema(group_type=type)} @@ -226,7 +226,7 @@ def read(self, id): except Exception, e: error_msg = "%s" % _("Cannot render description") c.description_formatted = genshi.HTML(error_msg) - + c.group_admins = self.authorizer.get_admins(c.group) context['return_query'] = True @@ -236,7 +236,7 @@ def read(self, id): page = int(request.params.get('page', 1)) except ValueError, e: abort(400, ('"page" parameter must be an integer')) - + # most search operations should reset the page counter: params_nopage = [(k, v) for k,v in request.params.items() if k != 'page'] @@ -250,9 +250,9 @@ def drill_down_url(**by): params = list(params_nopage) params.extend(by.items()) return search_url(set(params)) - - c.drill_down_url = drill_down_url - + + c.drill_down_url = drill_down_url + def remove_field(key, value): params = list(params_nopage) params.remove((key, value)) @@ -307,7 +307,7 @@ def pager_url(q=None, page=None): c.group_activity_stream = \ ckan.logic.action.get.group_activity_list_html(context, {'id': c.group_dict['id']}) - + return render('group/read.html') def new(self, data=None, errors=None, error_summary=None): @@ -316,11 +316,12 @@ def new(self, data=None, errors=None, error_summary=None): group_type = None if data: data['type'] = group_type - + context = {'model': model, 'session': model.Session, 'user': c.user or c.author, 'extras_as_string': True, 'schema': self._form_to_db_schema(), - 'save': 'save' in request.params } + 'save': 'save' in request.params, + 'parent': request.params.get('parent', None)} try: check_access('group_create',context) except NotAuthorized: @@ -328,7 +329,7 @@ def new(self, data=None, errors=None, error_summary=None): if context['save'] and not data: return self._save_new(context, group_type) - + data = data or {} errors = errors or {} error_summary = error_summary or {} @@ -339,11 +340,12 @@ def new(self, data=None, errors=None, error_summary=None): return render('group/new.html') def edit(self, id, data=None, errors=None, error_summary=None): - group_type = self._get_group_type(id.split('@')[0]) + group_type = self._get_group_type(id.split('@')[0]) context = {'model': model, 'session': model.Session, 'user': c.user or c.author, 'extras_as_string': True, 'save': 'save' in request.params, 'schema': self._form_to_db_schema(group_type=group_type), + 'parent': request.params.get('parent', None) } data_dict = {'id': id} @@ -381,9 +383,9 @@ def edit(self, id, data=None, errors=None, error_summary=None): def _get_group_type(self, id): """ - Given the id of a group it determines the plugin to load + Given the id of a group it determines the plugin to load based on the group's type name (type). The plugin found - will be returned, or None if there is no plugin associated with + will be returned, or None if there is no plugin associated with the type. Uses a minimal context to do so. The main use of this method @@ -392,7 +394,7 @@ def _get_group_type(self, id): aborts if an exception is raised. """ global _controller_behaviour_for - + context = {'model': model, 'session': model.Session, 'user': c.user or c.author} try: @@ -412,7 +414,7 @@ def _save_new(self, context, group_type=None): context['message'] = data_dict.get('log_message', '') data_dict['users'] = [{'name': c.user, 'capacity': 'admin'}] group = get_action('group_create')(context, data_dict) - + # Redirect to the appropriate _read route for the type of group h.redirect_to( group['type'] + '_read', id=group['name']) except NotAuthorized: @@ -427,7 +429,7 @@ def _save_new(self, context, group_type=None): return self.new(data_dict, errors, error_summary) def _save_edit(self, id, context): - try: + try: data_dict = clean_dict(unflatten( tuplize_dict(parse_params(request.params)))) context['message'] = data_dict.get('log_message', '') diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index 4c6b6da5e58..535bb337042 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -32,7 +32,7 @@ default_create_activity_schema) from ckan.logic.schema import default_group_schema, default_user_schema -from ckan.lib.navl.dictization_functions import validate +from ckan.lib.navl.dictization_functions import validate from ckan.logic.action.update import (_update_package_relationship, package_error_summary, group_error_summary, @@ -77,14 +77,14 @@ def package_create(context, data_dict): item.create(pkg) if not context.get('defer_commit'): - model.repo.commit() + model.repo.commit() ## need to let rest api create context["package"] = pkg - ## this is added so that the rest controller can make a new location + ## this is added so that the rest controller can make a new location context["id"] = pkg.id log.debug('Created object %s' % str(pkg.name)) - return package_dictize(pkg, context) + return package_dictize(pkg, context) def package_create_validate(context, data_dict): model = context['model'] @@ -92,7 +92,7 @@ def package_create_validate(context, data_dict): schema = context.get('schema') or default_create_package_schema() model.Session.remove() model.Session()._context = context - + check_access('package_create',context,data_dict) data, errors = validate(data_dict, schema, context) @@ -162,6 +162,7 @@ def group_create(context, data_dict): user = context['user'] session = context['session'] schema = context.get('schema') or default_group_schema() + parent = context.get('parent', None) check_access('group_create',context,data_dict) @@ -180,7 +181,13 @@ def group_create(context, data_dict): rev.message = _(u'REST API: Create object %s') % data.get("name") group = group_dict_save(data, context) - + + if parent: + parent_group = model.Group.get( parent ) + if parent_group: + member = model.Member(group=parent_group, table_id=group.id, table_name='group') + session.add(member) + if user: admins = [model.User.by_name(user.decode('utf8'))] else: @@ -209,7 +216,7 @@ def group_create(context, data_dict): activity_create(activity_create_context, activity_dict, ignore_auth=True) if not context.get('defer_commit'): - model.repo.commit() + model.repo.commit() context["group"] = group context["id"] = group.id log.debug('Created object %s' % str(group.name)) @@ -218,7 +225,7 @@ def group_create(context, data_dict): def rating_create(context, data_dict): model = context['model'] - user = context.get("user") + user = context.get("user") package_ref = data_dict.get('package') rating = data_dict.get('rating') @@ -294,13 +301,13 @@ def user_create(context, data_dict): ## Modifications for rest api def package_create_rest(context, data_dict): - + api = context.get('api_version') or '1' check_access('package_create_rest', context, data_dict) dictized_package = package_api_to_dict(data_dict, context) - dictized_after = package_create(context, dictized_package) + dictized_after = package_create(context, dictized_package) pkg = context['package'] @@ -320,7 +327,7 @@ def group_create_rest(context, data_dict): check_access('group_create_rest', context, data_dict) dictized_group = group_api_to_dict(data_dict, context) - dictized_after = group_create(context, dictized_group) + dictized_after = group_create(context, dictized_group) group = context['group'] diff --git a/ckan/logic/action/update.py b/ckan/logic/action/update.py index 4c5c4c7cae9..fe835fdcf5b 100644 --- a/ckan/logic/action/update.py +++ b/ckan/logic/action/update.py @@ -110,7 +110,7 @@ def _make_latest_rev_active(context, q): latest_rev.state = 'active' session.add(latest_rev) - + ##this is just a way to get the latest revision that changed ##in order to timestamp old_latest = context.get('latest_revision_date') @@ -159,10 +159,10 @@ def make_latest_pending_package_active(context, data_dict): revision = q.first() revision.approved_timestamp = datetime.datetime.now() session.add(revision) - + if not context.get('defer_commit'): - session.commit() - session.remove() + session.commit() + session.remove() def resource_update(context, data_dict): @@ -197,14 +197,14 @@ def resource_update(context, data_dict): resource = resource_dict_save(data, context) if not context.get('defer_commit'): - model.repo.commit() + model.repo.commit() return resource_dictize(resource, context) def package_update(context, data_dict): model = context['model'] user = context['user'] - + id = data_dict["id"] schema = context.get('schema') or default_update_package_schema() model.Session.remove() @@ -220,7 +220,7 @@ def package_update(context, data_dict): check_access('package_update', context, data_dict) data, errors = validate(data_dict, schema, context) - + if errors: model.Session.rollback() @@ -238,13 +238,13 @@ def package_update(context, data_dict): for item in PluginImplementations(IPackageController): item.edit(pkg) if not context.get('defer_commit'): - model.repo.commit() + model.repo.commit() return package_dictize(pkg, context) def package_update_validate(context, data_dict): model = context['model'] user = context['user'] - + id = data_dict["id"] schema = context.get('schema') or default_update_package_schema() model.Session.remove() @@ -276,7 +276,7 @@ def _update_package_relationship(relationship, comment, context): if is_changed: rev = model.repo.new_revision() rev.author = context["user"] - rev.message = (_(u'REST API: Update package relationship: %s %s %s') % + rev.message = (_(u'REST API: Update package relationship: %s %s %s') % (relationship.subject, relationship.type, relationship.object)) relationship.comment = comment if not context.get('defer_commit'): @@ -326,6 +326,7 @@ def group_update(context, data_dict): session = context['session'] schema = context.get('schema') or default_update_group_schema() id = data_dict['id'] + parent = context.get('parent', None) group = model.Group.get(id) context["group"] = group @@ -341,7 +342,7 @@ def group_update(context, data_dict): rev = model.repo.new_revision() rev.author = user - + if 'message' in context: rev.message = context['message'] else: @@ -349,6 +350,19 @@ def group_update(context, data_dict): group = group_dict_save(data, context) + if parent: + parent_group = model.Group.get( parent ) + if parent_group and not parent_group in group.get_groups(group.type): + # Delete all of this groups memberships + current = session.query(model.Member).\ + filter(model.Member.table_id == group.id).\ + filter(model.Member.table_name == "group").all() + for c in current: + session.delete(c) + member = model.Member(group=parent_group, table_id=group.id, table_name='group') + session.add(member) + + for item in PluginImplementations(IGroupController): item.edit(group) @@ -388,7 +402,7 @@ def group_update(context, data_dict): # in the group. if not context.get('defer_commit'): - model.repo.commit() + model.repo.commit() return group_dictize(group, context) @@ -398,7 +412,7 @@ def user_update(context, data_dict): model = context['model'] user = context['user'] session = context['session'] - schema = context.get('schema') or default_update_user_schema() + schema = context.get('schema') or default_update_user_schema() id = data_dict['id'] user_obj = model.User.get(id) @@ -432,7 +446,7 @@ def user_update(context, data_dict): # the user. if not context.get('defer_commit'): - model.repo.commit() + model.repo.commit() return user_dictize(user, context) def task_status_update(context, data_dict): @@ -450,7 +464,7 @@ def task_status_update(context, data_dict): if task_status is None: raise NotFound(_('TaskStatus was not found.')) - + check_access('task_status_update', context, data_dict) data, errors = validate(data_dict, schema, context) @@ -584,7 +598,7 @@ def user_role_update(context, data_dict): else: user_object = model.AuthorizationGroup.get(new_authgroup_ref) if not user_object: - raise NotFound('Cannot find authorization group %r' % new_authgroup_ref) + raise NotFound('Cannot find authorization group %r' % new_authgroup_ref) data_dict['authorization_group'] = user_object.id add_user_to_role_func = model.add_authorization_group_to_role remove_user_from_role_func = model.remove_authorization_group_from_role From 64900fae93d617f442b46a250e50d9c083644916 Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Tue, 14 Feb 2012 15:36:08 +0000 Subject: [PATCH 58/59] [1794] Fixed dodgy migration name --- ...group_approval_status.py => 049_add_group_approval_status.py} | 1 - 1 file changed, 1 deletion(-) rename ckan/migration/versions/{0049_add_group_approval_status.py => 049_add_group_approval_status.py} (99%) diff --git a/ckan/migration/versions/0049_add_group_approval_status.py b/ckan/migration/versions/049_add_group_approval_status.py similarity index 99% rename from ckan/migration/versions/0049_add_group_approval_status.py rename to ckan/migration/versions/049_add_group_approval_status.py index bbc6d1d09ff..367407b0d29 100644 --- a/ckan/migration/versions/0049_add_group_approval_status.py +++ b/ckan/migration/versions/049_add_group_approval_status.py @@ -8,7 +8,6 @@ def upgrade(migrate_engine): ALTER TABLE group_revision ADD COLUMN approval_status text; - update "group" set approval_status = 'approved'; update group_revision set approval_status = 'approved'; From 00c071ff8cb972201e582ca98e99a4f2f948750e Mon Sep 17 00:00:00 2001 From: Ross Jones Date: Wed, 15 Feb 2012 16:06:30 +0000 Subject: [PATCH 59/59] [xs] Fixing publisher group test that had not run under sqlite --- ckan/tests/functional/test_group.py | 67 ++++++++++++++--------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/ckan/tests/functional/test_group.py b/ckan/tests/functional/test_group.py index 59d745f2df4..9707e8889af 100644 --- a/ckan/tests/functional/test_group.py +++ b/ckan/tests/functional/test_group.py @@ -7,7 +7,7 @@ import ckan.model as model from ckan.lib.create_test_data import CreateTestData from ckan.logic import check_access, NotAuthorized - + from pylons import config from ckan.tests import * @@ -17,11 +17,11 @@ class MockGroupControllerPlugin(SingletonPlugin): implements(IGroupController) - + def __init__(self): from collections import defaultdict self.calls = defaultdict(int) - + def read(self, entity): self.calls['read'] += 1 @@ -77,7 +77,7 @@ def test_index(self): self.check_named_element(res, 'tr', group_title, group_packages_count, group_description) res = res.click(group_title) assert groupname in res - + def test_read_non_existent(self): name = u'group_does_not_exist' offset = url_for(controller='group', action='read', id=name) @@ -189,7 +189,7 @@ def test_2_edit(self): res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'}) # should be read page # assert 'Groups - %s' % self.groupname in res, res - + model.Session.remove() group = model.Group.by_name(self.groupname) assert group.title == newtitle, group @@ -214,11 +214,11 @@ def test_3_edit_form_has_new_package(self): user = model.User.by_name(u'russianfan') model.setup_default_user_roles(pkg, [user]) model.repo.commit_and_remove() - + res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) assert 'annakarenina' in res, res assert 'newone' in res - + def test_4_new_duplicate_package(self): prefix = '' @@ -265,14 +265,14 @@ def test_delete(self): CreateTestData.create_groups([{'name': group_name, 'packages': [self.packagename]}], admin_user_name='russianfan') - + group = model.Group.by_name(group_name) offset = url_for(controller='group', action='edit', id=group_name) res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'}) main_res = self.main_div(res) assert 'Edit: %s' % group.title in main_res, main_res assert 'value="active" selected' in main_res, main_res - + # delete form = res.forms['group-edit'] form['state'] = 'deleted' @@ -291,7 +291,7 @@ class TestNew(FunctionalTestCase): def setup_class(self): model.Session.remove() CreateTestData.create() - + self.packagename = u'testpkg' model.repo.new_revision() model.Session.add(model.Package(name=self.packagename)) @@ -372,7 +372,7 @@ def test_3_new_duplicate_group(self): assert 'Group name already exists' in res, res self.check_tag(res, '