From c08955e9a517abf53592ca59fc83a40d1b1cb976 Mon Sep 17 00:00:00 2001 From: Nigel Babu Date: Mon, 25 Mar 2013 10:33:04 +0530 Subject: [PATCH 001/175] Test to find undocumented action functions --- ckan/tests/logic/test_action.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ckan/tests/logic/test_action.py b/ckan/tests/logic/test_action.py index b63e0ca6067..ca3b5c8494e 100644 --- a/ckan/tests/logic/test_action.py +++ b/ckan/tests/logic/test_action.py @@ -2,6 +2,7 @@ import json import urllib from pprint import pprint +import types from nose.tools import assert_equal, assert_raises from nose.plugins.skip import SkipTest from pylons import config @@ -1555,3 +1556,21 @@ def test_02_bulk_delete(self): assert json.loads(res.body)['result']['count'] == 0 +class TestDocStrings: + + def test_docstrings_in_action(self): + func_with_no_docstring = [] + for action_module_name in ['get', 'create', 'update', 'delete']: + module_path = 'ckan.logic.action.' + action_module_name + module = __import__(module_path) + for part in module_path.split('.')[1:]: + module = getattr(module, part) + for k, v in module.__dict__.items(): + if not k.startswith('_'): + # Only load functions from the action module. + if isinstance(v, types.FunctionType): + if v.__doc__ is None: + func_with_no_docstring.append(k) + print func_with_no_docstring + assert len(func_with_no_docstring) == 0 + From 427b852e3fc386f93907ecf36900bdbcaae7fce5 Mon Sep 17 00:00:00 2001 From: Nigel Babu Date: Mon, 25 Mar 2013 11:18:33 +0530 Subject: [PATCH 002/175] Documentation for member_roles_list --- ckan/logic/action/get.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 45417d8f8a6..2738f5592f7 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -834,7 +834,7 @@ def _group_or_org_show(context, data_dict, is_org=False): _check_access('organization_show',context, data_dict) else: _check_access('group_show',context, data_dict) - + group_dict = model_dictize.group_dictize(group, context) @@ -2702,4 +2702,11 @@ def _unpick_search(sort, allowed_fields=None, total=None): def member_roles_list(context, data_dict): + '''Return the list of roles. + + Return the list of roles for use in forms. + + :rtype: list of dictionaries + + ''' return new_authz.roles_list() From 5968443f7f6d8726be850e8cf312977ccd9affdb Mon Sep 17 00:00:00 2001 From: Nigel Babu Date: Mon, 25 Mar 2013 11:18:47 +0530 Subject: [PATCH 003/175] Fix import for asbool --- ckan/logic/action/create.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index df259500790..3a389e0b83a 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -1,7 +1,7 @@ import logging from pylons import config from pylons.i18n import _ -from paste.deploy.converters import asbool +import paste.deploy.converters import ckan.new_authz as new_authz import ckan.lib.plugins as lib_plugins @@ -891,7 +891,8 @@ def activity_create(context, activity_dict, ignore_auth=False): :rtype: dictionary ''' - if not asbool(config.get('ckan.activity_streams_enabled', 'true')): + if not paste.deploy.converters.asbool( + config.get('ckan.activity_streams_enabled', 'true')): return model = context['model'] From 2cb65e9fb4efc6fae37f2fcd7d4f3de7e7014690 Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Wed, 27 Mar 2013 14:02:50 +0100 Subject: [PATCH 004/175] [#707] Implement group_purge action function This is a rough first implementation, needs tests. --- ckan/logic/action/delete.py | 38 +++++++++++++++++++++++++++++++++++++ ckan/logic/auth/delete.py | 8 ++++++++ 2 files changed, 46 insertions(+) diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index 8c4a7dd8cde..2d83dd88c86 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -262,6 +262,44 @@ def organization_delete(context, data_dict): ''' return _group_or_org_delete(context, data_dict, is_org=True) +def _group_or_org_purge(context, data_dict, is_org=False): + model = context['model'] + id = _get_or_bust(data_dict, 'id') + if is_org: + group_or_org = 'organization' + else: + group_or_org = 'group' + + group = model.Group.get(id) + context['group'] = group + if group is None: + raise NotFound('{group_or_org} was not found'.format( + group_or_org=group_or_org.capitalize())) + + if is_org: + _check_access('organization_purge', context, data_dict) + else: + _check_access('group_purge', context, data_dict) + + members = model.Session.query(model.Member) + members = members.filter(model.Member.group_id == group.id) + if members.count() > 0: + model.repo.new_revision() + for m in members.all(): + m.delete() + model.repo.commit_and_remove() + + group = model.Group.get(id) + model.repo.new_revision() + group.purge() + model.repo.commit_and_remove() + +def group_purge(context, data_dict): + return _group_or_org_purge(context, data_dict, is_org=False) + +def organization_purge(context, data_dict): + return _group_or_org_purge(context, data_dict, is_org=True) + def task_status_delete(context, data_dict): '''Delete a task status. diff --git a/ckan/logic/auth/delete.py b/ckan/logic/auth/delete.py index 4ccd7acc693..0f156f0058d 100644 --- a/ckan/logic/auth/delete.py +++ b/ckan/logic/auth/delete.py @@ -84,6 +84,14 @@ def group_delete(context, data_dict): else: return {'success': True} +def group_purge(): + # Only sysadmins are authorized to purge groups. + return {'success': False} + +def organization_purge(): + # Only sysadmins are authorized to purge organizations. + return {'success': False} + def organization_delete(context, data_dict): group = get_group_object(context, data_dict) user = context['user'] From 2933dc28c2d609b84845bed1182160c870a83b6a Mon Sep 17 00:00:00 2001 From: Sean Hammond Date: Thu, 28 Mar 2013 10:16:09 +0100 Subject: [PATCH 005/175] [#707] Add args to group and org purge auth functions Oops :) --- ckan/logic/auth/delete.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/logic/auth/delete.py b/ckan/logic/auth/delete.py index 0f156f0058d..fd24b74afe2 100644 --- a/ckan/logic/auth/delete.py +++ b/ckan/logic/auth/delete.py @@ -84,11 +84,11 @@ def group_delete(context, data_dict): else: return {'success': True} -def group_purge(): +def group_purge(context, data_dict): # Only sysadmins are authorized to purge groups. return {'success': False} -def organization_purge(): +def organization_purge(context, data_dict): # Only sysadmins are authorized to purge organizations. return {'success': False} From 24f686da6cfee95b200e93ec91fafa8949a5de15 Mon Sep 17 00:00:00 2001 From: tobes Date: Tue, 23 Apr 2013 16:22:22 +0100 Subject: [PATCH 006/175] [#792] Fix auth function --- ckan/logic/auth/create.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ckan/logic/auth/create.py b/ckan/logic/auth/create.py index 1baf9b61077..8832651601f 100644 --- a/ckan/logic/auth/create.py +++ b/ckan/logic/auth/create.py @@ -10,6 +10,7 @@ def package_create(context, data_dict=None): check1 = new_authz.check_config_permission('anon_create_dataset') else: check1 = new_authz.check_config_permission('create_dataset_if_not_in_organization') \ + or new_authz.check_config_permission('create_unowned_dataset') \ or new_authz.has_user_permission_for_some_org(user, 'create_dataset') if not check1: From 1567fee20dfa4cdf82e2994697f3e8030063ffca Mon Sep 17 00:00:00 2001 From: Nigel Babu Date: Mon, 13 May 2013 09:35:36 +0530 Subject: [PATCH 007/175] Document group_member_create --- ckan/logic/action/create.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index 776c602849a..a6740c60f20 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -1,7 +1,7 @@ import logging from pylons import config -from paste.deploy.converters import asbool +import paste.deploy.converters import ckan.lib.plugins as lib_plugins import ckan.logic as logic @@ -1133,6 +1133,17 @@ def _group_or_org_member_create(context, data_dict, is_org=False): logic.get_action('member_create')(member_create_context, member_dict) def group_member_create(context, data_dict): + '''Make a user a member of a group + + You must be authorized to edit the group. + + :param id: the id or name of the group + :param username: name or id of the user to be made member of the group + :param role: role of the user in the group. One of member, editor, or admin. + + :returns: the newly created (or updated) membership + :rtype: dictionary + ''' _check_access('group_member_create', context, data_dict) return _group_or_org_member_create(context, data_dict) From 129644f7ad1a9c4066966d6e44fca98131065b14 Mon Sep 17 00:00:00 2001 From: Nigel Babu Date: Mon, 13 May 2013 09:45:08 +0530 Subject: [PATCH 008/175] Added documentation for group_member_delete Updated documentation for group_member_create --- ckan/logic/action/create.py | 3 ++- ckan/logic/action/delete.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index a6740c60f20..7f0034dd286 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -1139,7 +1139,8 @@ def group_member_create(context, data_dict): :param id: the id or name of the group :param username: name or id of the user to be made member of the group - :param role: role of the user in the group. One of member, editor, or admin. + :param role: role of the user in the group. One of ``member``, ``editor``, + or ``admin``. :returns: the newly created (or updated) membership :rtype: dictionary diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index 10b97de06c5..e866bfad55a 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -433,6 +433,14 @@ def _group_or_org_member_delete(context, data_dict=None): def group_member_delete(context, data_dict=None): + '''Remove a user from a group + + You must be authorized to edit the group. + + :param id: the id or name of the group + :param username: name or id of the user to be removed + + ''' return _group_or_org_member_delete(context, data_dict) def organization_member_delete(context, data_dict=None): From 550d2a5789b014c87576089d816c3cd4509aa21e Mon Sep 17 00:00:00 2001 From: Nigel Babu Date: Mon, 13 May 2013 10:20:36 +0530 Subject: [PATCH 009/175] [#709] Document pckage_relationships_list() --- ckan/logic/action/get.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index de7b8de3baf..1c8ae2530e2 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -658,12 +658,10 @@ def user_list(context, data_dict): def package_relationships_list(context, data_dict): '''Return a dataset (package)'s relationships. - :param id: the id or name of the package - :type id: string - :param id2: - :type id2: - :param rel: - :type rel: + :param id: the id or name of the first package + :param id2: the id or name of the second package + :param rel: relationship as string see ``package_relationship_create()`` + for the relationship types (optional) :rtype: list of dictionaries From 4d95bbc8a0dda2a1d724fca77b9cc6c8b4aa36e0 Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Mon, 27 May 2013 21:56:26 +0200 Subject: [PATCH 010/175] [#907] Always create the type `nested` with a write connection --- ckanext/datastore/db.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ckanext/datastore/db.py b/ckanext/datastore/db.py index f1d8818438e..a084a9ee8a8 100644 --- a/ckanext/datastore/db.py +++ b/ckanext/datastore/db.py @@ -136,12 +136,16 @@ def _cache_types(context): if 'nested' not in _type_names: native_json = _pg_version_is_at_least(connection, '9.2') - connection.execute('CREATE TYPE "nested" AS (json {0}, extra text)' - .format('json' if native_json else 'text')) + log.info("Create nested type. Native JSON: {0}".format(native_json)) + + import pylons + data_dict = {'connection_url': pylons.config['ckan.datastore.write_url']} + engine = _get_engine(None, data_dict) + with engine.begin() as connection: + connection.execute('CREATE TYPE "nested" AS (json {0}, extra text)' + .format('json' if native_json else 'text')) _pg_types.clear() - log.info("Created nested type. Native JSON: {0}".format(native_json)) - ## redo cache types with json now available. return _cache_types(context) From 05e6e1820d51187937265d8c544ef424dfc1805a Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Mon, 27 May 2013 21:57:57 +0200 Subject: [PATCH 011/175] [#907] Remove unused argument context from _get_engine --- ckanext/datastore/db.py | 14 +++++++------- ckanext/datastore/logic/action.py | 6 +++--- ckanext/datastore/plugin.py | 10 ++++------ ckanext/datastore/tests/test_create.py | 1 - ckanext/datastore/tests/test_delete.py | 1 - ckanext/datastore/tests/test_search.py | 1 - ckanext/datastore/tests/test_unit.py | 2 +- ckanext/datastore/tests/test_upsert.py | 3 --- 8 files changed, 15 insertions(+), 23 deletions(-) diff --git a/ckanext/datastore/db.py b/ckanext/datastore/db.py index a084a9ee8a8..28326b0d1cb 100644 --- a/ckanext/datastore/db.py +++ b/ckanext/datastore/db.py @@ -113,7 +113,7 @@ def _validate_int(i, field_name, non_negative=False): }) -def _get_engine(context, data_dict): +def _get_engine(data_dict): '''Get either read or write engine.''' connection_url = data_dict['connection_url'] engine = _engines.get(connection_url) @@ -140,7 +140,7 @@ def _cache_types(context): import pylons data_dict = {'connection_url': pylons.config['ckan.datastore.write_url']} - engine = _get_engine(None, data_dict) + engine = _get_engine(data_dict) with engine.begin() as connection: connection.execute('CREATE TYPE "nested" AS (json {0}, extra text)' .format('json' if native_json else 'text')) @@ -946,7 +946,7 @@ def create(context, data_dict): Any error results in total failure! For now pass back the actual error. Should be transactional. ''' - engine = _get_engine(context, data_dict) + engine = _get_engine(data_dict) context['connection'] = engine.connect() timeout = context.get('query_timeout', 60000) _cache_types(context) @@ -1002,7 +1002,7 @@ def upsert(context, data_dict): Any error results in total failure! For now pass back the actual error. Should be transactional. ''' - engine = _get_engine(context, data_dict) + engine = _get_engine(data_dict) context['connection'] = engine.connect() timeout = context.get('query_timeout', 60000) @@ -1038,7 +1038,7 @@ def upsert(context, data_dict): def delete(context, data_dict): - engine = _get_engine(context, data_dict) + engine = _get_engine(data_dict) context['connection'] = engine.connect() _cache_types(context) @@ -1071,7 +1071,7 @@ def delete(context, data_dict): def search(context, data_dict): - engine = _get_engine(context, data_dict) + engine = _get_engine(data_dict) context['connection'] = engine.connect() timeout = context.get('query_timeout', 60000) _cache_types(context) @@ -1102,7 +1102,7 @@ def search(context, data_dict): def search_sql(context, data_dict): - engine = _get_engine(context, data_dict) + engine = _get_engine(data_dict) context['connection'] = engine.connect() timeout = context.get('query_timeout', 60000) _cache_types(context) diff --git a/ckanext/datastore/logic/action.py b/ckanext/datastore/logic/action.py index 1b4d323e7f2..aebc96df283 100644 --- a/ckanext/datastore/logic/action.py +++ b/ckanext/datastore/logic/action.py @@ -114,7 +114,7 @@ def datastore_upsert(context, data_dict): resources_sql = sqlalchemy.text(u'''SELECT 1 FROM "_table_metadata" WHERE name = :id AND alias_of IS NULL''') - results = db._get_engine(None, data_dict).execute(resources_sql, id=res_id) + results = db._get_engine(data_dict).execute(resources_sql, id=res_id) res_exists = results.rowcount > 0 if not res_exists: @@ -153,7 +153,7 @@ def datastore_delete(context, data_dict): resources_sql = sqlalchemy.text(u'''SELECT 1 FROM "_table_metadata" WHERE name = :id AND alias_of IS NULL''') - results = db._get_engine(None, data_dict).execute(resources_sql, id=res_id) + results = db._get_engine(data_dict).execute(resources_sql, id=res_id) res_exists = results.rowcount > 0 if not res_exists: @@ -228,7 +228,7 @@ def datastore_search(context, data_dict): pylons.config['ckan.datastore.write_url']) resources_sql = sqlalchemy.text(u'SELECT 1 FROM "_table_metadata" WHERE name = :id') - results = db._get_engine(None, data_dict).execute(resources_sql, id=res_id) + results = db._get_engine(data_dict).execute(resources_sql, id=res_id) res_exists = results.rowcount > 0 if not res_exists: diff --git a/ckanext/datastore/plugin.py b/ckanext/datastore/plugin.py index 50093d88ce4..44412d8a360 100644 --- a/ckanext/datastore/plugin.py +++ b/ckanext/datastore/plugin.py @@ -77,7 +77,6 @@ def configure(self, config): @logic.side_effect_free def new_resource_show(context, data_dict): engine = db._get_engine( - context, {'connection_url': self.read_url} ) new_data_dict = resource_show(context, data_dict) @@ -130,8 +129,7 @@ def _is_read_only_database(self): ''' Returns True if no connection has CREATE privileges on the public schema. This is the case if replication is enabled.''' for url in [self.ckan_url, self.write_url, self.read_url]: - connection = db._get_engine(None, - {'connection_url': url}).connect() + connection = db._get_engine({'connection_url': url}).connect() try: sql = u"SELECT has_schema_privilege('public', 'CREATE')" is_writable = connection.execute(sql).first()[0] @@ -155,9 +153,9 @@ def _read_connection_has_correct_privileges(self): ''' Returns True if the right permissions are set for the read only user. A table is created by the write user to test the read only user. ''' - write_connection = db._get_engine(None, + write_connection = db._get_engine( {'connection_url': self.write_url}).connect() - read_connection = db._get_engine(None, + read_connection = db._get_engine( {'connection_url': self.read_url}).connect() drop_foo_sql = u'DROP TABLE IF EXISTS _foo' @@ -202,7 +200,7 @@ def _create_alias_table(self): ''' create_alias_table_sql = u'CREATE OR REPLACE VIEW "_table_metadata" AS {0}'.format(mapping_sql) try: - connection = db._get_engine(None, + connection = db._get_engine( {'connection_url': pylons.config['ckan.datastore.write_url']}).connect() connection.execute(create_alias_table_sql) finally: diff --git a/ckanext/datastore/tests/test_create.py b/ckanext/datastore/tests/test_create.py index ae5621c8c18..4ab2cd59058 100644 --- a/ckanext/datastore/tests/test_create.py +++ b/ckanext/datastore/tests/test_create.py @@ -27,7 +27,6 @@ def setup_class(cls): cls.normal_user = model.User.get('annafan') import pylons engine = db._get_engine( - None, {'connection_url': pylons.config['ckan.datastore.write_url']} ) cls.Session = orm.scoped_session(orm.sessionmaker(bind=engine)) diff --git a/ckanext/datastore/tests/test_delete.py b/ckanext/datastore/tests/test_delete.py index f4522350272..dcbc07b8ada 100644 --- a/ckanext/datastore/tests/test_delete.py +++ b/ckanext/datastore/tests/test_delete.py @@ -41,7 +41,6 @@ def setup_class(cls): import pylons engine = db._get_engine( - None, {'connection_url': pylons.config['ckan.datastore.write_url']} ) cls.Session = orm.scoped_session(orm.sessionmaker(bind=engine)) diff --git a/ckanext/datastore/tests/test_search.py b/ckanext/datastore/tests/test_search.py index c4b58013112..c35675baa34 100644 --- a/ckanext/datastore/tests/test_search.py +++ b/ckanext/datastore/tests/test_search.py @@ -62,7 +62,6 @@ def setup_class(cls): import pylons engine = db._get_engine( - None, {'connection_url': pylons.config['ckan.datastore.write_url']} ) cls.Session = orm.scoped_session(orm.sessionmaker(bind=engine)) diff --git a/ckanext/datastore/tests/test_unit.py b/ckanext/datastore/tests/test_unit.py index 2f0e8c0d750..c9c8a11c1c7 100644 --- a/ckanext/datastore/tests/test_unit.py +++ b/ckanext/datastore/tests/test_unit.py @@ -56,7 +56,7 @@ def test_is_valid_table_name(self): def test_pg_version_check(self): if not tests.is_datastore_supported(): raise nose.SkipTest("Datastore not supported") - engine = db._get_engine(None, + engine = db._get_engine( {'connection_url': pylons.config['sqlalchemy.url']}) connection = engine.connect() assert db._pg_version_is_at_least(connection, '8.0') diff --git a/ckanext/datastore/tests/test_upsert.py b/ckanext/datastore/tests/test_upsert.py index 2f4a48146fa..87685d99e60 100644 --- a/ckanext/datastore/tests/test_upsert.py +++ b/ckanext/datastore/tests/test_upsert.py @@ -49,7 +49,6 @@ def setup_class(cls): import pylons engine = db._get_engine( - None, {'connection_url': pylons.config['ckan.datastore.write_url']} ) cls.Session = orm.scoped_session(orm.sessionmaker(bind=engine)) @@ -275,7 +274,6 @@ def setup_class(cls): import pylons engine = db._get_engine( - None, {'connection_url': pylons.config['ckan.datastore.write_url']} ) cls.Session = orm.scoped_session(orm.sessionmaker(bind=engine)) @@ -382,7 +380,6 @@ def setup_class(cls): import pylons engine = db._get_engine( - None, {'connection_url': pylons.config['ckan.datastore.write_url']} ) cls.Session = orm.scoped_session(orm.sessionmaker(bind=engine)) From 8fb02d0c038109a70c2e351dcd4a60bb16bcf9eb Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Mon, 27 May 2013 23:31:53 +0200 Subject: [PATCH 012/175] [#907] test_unit does not require blacklisting any more --- ckan/tests/test_coding_standards.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ckan/tests/test_coding_standards.py b/ckan/tests/test_coding_standards.py index 024982f5ca4..2bdf2a71e66 100644 --- a/ckan/tests/test_coding_standards.py +++ b/ckan/tests/test_coding_standards.py @@ -760,7 +760,6 @@ class TestPep8(object): 'ckanext/datastore/tests/test_create.py', 'ckanext/datastore/tests/test_delete.py', 'ckanext/datastore/tests/test_search.py', - 'ckanext/datastore/tests/test_unit.py', 'ckanext/datastore/tests/test_upsert.py', 'ckanext/example_idatasetform/plugin.py', 'ckanext/example_itemplatehelpers/plugin.py', From 50552bed67617c1404c7a96a680c465e406daf18 Mon Sep 17 00:00:00 2001 From: Nigel Babu Date: Tue, 28 May 2013 12:18:36 +0530 Subject: [PATCH 013/175] Add docstrings for two more functions * Add docstring for organization_member-create. * Add doctstring for organization_member_delete. --- ckan/logic/action/create.py | 13 +++++++++++++ ckan/logic/action/delete.py | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/ckan/logic/action/create.py b/ckan/logic/action/create.py index 5b5576fd626..977d5f66038 100644 --- a/ckan/logic/action/create.py +++ b/ckan/logic/action/create.py @@ -1152,6 +1152,19 @@ def group_member_create(context, data_dict): return _group_or_org_member_create(context, data_dict) def organization_member_create(context, data_dict): + '''Make a user a member of an organization + + You must be authorized to edit the organization. + + :param id: the id or name of the organization + :param username: name or id of the user to be made member of the + organization + :param role: role of the user in the organization. One of ``member``, + ``editor``, or ``admin``. + + :returns: the newly created (or updated) membership + :rtype: dictionary + ''' _check_access('organization_member_create', context, data_dict) return _group_or_org_member_create(context, data_dict, is_org=True) diff --git a/ckan/logic/action/delete.py b/ckan/logic/action/delete.py index e866bfad55a..39cbd7e4cf1 100644 --- a/ckan/logic/action/delete.py +++ b/ckan/logic/action/delete.py @@ -444,6 +444,14 @@ def group_member_delete(context, data_dict=None): return _group_or_org_member_delete(context, data_dict) def organization_member_delete(context, data_dict=None): + '''Remove a user from a organization + + You must be authorized to edit the organization. + + :param id: the id or name of the organization + :param username: name or id of the user to be removed + + ''' return _group_or_org_member_delete(context, data_dict) From 6322751558babe1bcacaf4ef8726a90087a0a830 Mon Sep 17 00:00:00 2001 From: John Martin Date: Wed, 29 May 2013 11:44:37 +0100 Subject: [PATCH 014/175] [#626] Tweaks dashboard to have 3 new subpages A my datasets, my orgs and my groups pages. Each one showing ownership of the relevant items within it. --- ckan/config/routing.py | 8 +++- ckan/controllers/user.py | 27 ++++++++++++ ckan/public/base/less/dashboard.less | 17 +++++++- ckan/public/base/less/nav.less | 33 ++++++++++---- ckan/templates/user/dashboard.html | 43 ++++++++----------- ckan/templates/user/dashboard_datasets.html | 23 ++++++++++ ckan/templates/user/dashboard_groups.html | 23 ++++++++++ .../user/dashboard_organizations.html | 23 ++++++++++ 8 files changed, 162 insertions(+), 35 deletions(-) create mode 100644 ckan/templates/user/dashboard_datasets.html create mode 100644 ckan/templates/user/dashboard_groups.html create mode 100644 ckan/templates/user/dashboard_organizations.html diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 0d3c00c5838..d42e508f5e2 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -343,9 +343,15 @@ def make_map(): m.connect('/user/activity/{id}/{offset}', action='activity') m.connect('user_activity_stream', '/user/activity/{id}', action='activity', ckan_icon='time') - m.connect('/dashboard/{offset}', action='dashboard') m.connect('user_dashboard', '/dashboard', action='dashboard', ckan_icon='list') + m.connect('user_dashboard_datasets', '/dashboard/datasets', + action='dashboard_datasets', ckan_icon='sitemap') + m.connect('user_dashboard_groups', '/dashboard/groups', + action='dashboard_groups', ckan_icon='group') + m.connect('user_dashboard_organizations', '/dashboard/organizations', + action='dashboard_organizations', ckan_icon='building') + m.connect('/dashboard/{offset}', action='dashboard') m.connect('user_follow', '/user/follow/{id}', action='follow') m.connect('/user/unfollow/{id}', action='unfollow') m.connect('user_followers', '/user/followers/{id:.*}', diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py index 64ac13818b9..4761af49ced 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -583,6 +583,33 @@ def dashboard(self, id=None, offset=0): return render('user/dashboard.html') + def dashboard_datasets(self): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'for_view': True} + data_dict = {'user_obj': c.userobj} + self._setup_template_variables(context, data_dict) + return render('user/dashboard_datasets.html') + + def dashboard_organizations(self): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'for_view': True} + data_dict = {'user_obj': c.userobj} + self._setup_template_variables(context, data_dict) + # TODO: Add organizations that user is a member of here. I tried + # h.organizations_available() but that errors in the template + c.organizations = []#h.organizations_available() + return render('user/dashboard_organizations.html') + + def dashboard_groups(self): + context = {'model': model, 'session': model.Session, + 'user': c.user or c.author, 'for_view': True} + data_dict = {'user_obj': c.userobj} + self._setup_template_variables(context, data_dict) + # TODO: Add groups that user is a member of here. I tried + # h.groups_available() but that errors in the template + c.groups = []#h.groups_available() + return render('user/dashboard_groups.html') + def follow(self, id): '''Start following this user.''' context = {'model': model, diff --git a/ckan/public/base/less/dashboard.less b/ckan/public/base/less/dashboard.less index 4e8b047561c..286f72bb968 100644 --- a/ckan/public/base/less/dashboard.less +++ b/ckan/public/base/less/dashboard.less @@ -33,7 +33,7 @@ .arrow { position: absolute; content: ' '; - top: 70px; + top: 30px; right: -10px; width: 10px; height: 21px; @@ -113,3 +113,18 @@ } } } + +.dashboard-me { + .clearfix; + padding: 15px 15px 0 15px; + img { + float: left; + margin-right: 10px; + .border-radius(100px); + } + strong { + display: block; + font-size: 16px; + margin: 3px 0; + } +} diff --git a/ckan/public/base/less/nav.less b/ckan/public/base/less/nav.less index f49b8101c29..44e9e7e7e62 100644 --- a/ckan/public/base/less/nav.less +++ b/ckan/public/base/less/nav.less @@ -1,9 +1,17 @@ -.nav-simple { +.nav-simple, +.nav-aside { .simple-list; // Adds border and padding. padding-bottom: 0; } -.nav-item > a { +.nav-aside { + border-top: 1px dotted #DDD; + border-bottom: 1px dotted #DDD; + margin-bottom: 15px; +} + +.nav-item > a, +.nav-aside li a { color: @navLinkColor; font-size: @baseFontSize; line-height: @baseLineHeight; @@ -11,11 +19,13 @@ padding: 7px @gutterX; } -.nav-item.active { +.nav-item.active, +.nav-aside li.active { background-color: @navActiveBackgroundColor; } -.nav-item.active > a { +.nav-item.active > a, +.nav-aside li.active a { position: relative; color: @navItemActiveTextColor; background-color: @navItemActiveBackgroundColor; @@ -34,28 +44,33 @@ } } -.nav-item.active > a span { +.nav-item.active > a span, +.nav-aside li.active a span { white-space: nowrap; overflow:hidden; display:block; } -.module-narrow .nav-item > a { +.module-narrow .nav-item > a, +.module-narrow .nav-aside li a { padding-left: @gutterSmallX; padding-right: @gutterSmallX; position: relative; } -.module-narrow .nav-item.image { +.module-narrow .nav-item.image, +.module-narrow .nav-aside li.image { position: relative; } -.module-narrow .nav-item.image > a { +.module-narrow .nav-item.image > a, +.module-narrow .nav-aside li.image a { padding-left: @gutterSmallX + 27; padding-right: @gutterSmallX + 27; } -.module-narrow .nav-item.image > img { +.module-narrow .nav-item.image > img, +.module-narrow .nav-aside li.image img { position: absolute; top: 50%; left: @gutterSmallX; diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html index 5ee3e1edffd..87bfd0e497c 100644 --- a/ckan/templates/user/dashboard.html +++ b/ckan/templates/user/dashboard.html @@ -1,9 +1,7 @@ {% extends "user/edit_base.html" %} -{% block add_action_content %} - {% if h.check_access('package_create') %} -
  • {% link_for _('Add Dataset'), controller='package', action='new', class_="btn btn-primary", icon="plus-sign-alt" %}
  • - {% endif %} +{% block breadcrumb_content %} +
  • {{ _('Dashboard') }}
  • {% endblock %} {% block primary_content_inner %} @@ -30,26 +28,23 @@

    {% endif %} {% endblock %} - {% block dashboard_datasets %} -
    -

    - - {{ _('My Datasets') }} -

    - {% if c.user_dict['datasets'] %} -
    {% endblock %} {% endblock %} diff --git a/ckan/templates/user/dashboard_datasets.html b/ckan/templates/user/dashboard_datasets.html new file mode 100644 index 00000000000..0be60193ddc --- /dev/null +++ b/ckan/templates/user/dashboard_datasets.html @@ -0,0 +1,23 @@ +{% extends "user/dashboard.html" %} + +{% block dashboard_activity_stream_context %}{% endblock %} + +{% block add_action_content %} + {% if h.check_access('package_create') %} +
  • {% link_for _('Add Dataset'), controller='package', action='new', class_="btn btn-primary", icon="plus-sign-alt" %}
  • + {% endif %} +{% endblock %} + +{% block primary_content_inner %} +

    {{ _('My Datasets') }}

    + {% if c.user_dict.datasets %} + {% snippet 'snippets/package_list.html', packages=c.user_dict.datasets %} + {% else %} +

    + {{ _('You haven\'t created any datasets.') }} + {% if h.check_access('package_create') %} + {% link_for _('Create one now?'), controller='package', action='new' %} + {% endif %} +

    + {% endif %} +{% endblock %} diff --git a/ckan/templates/user/dashboard_groups.html b/ckan/templates/user/dashboard_groups.html new file mode 100644 index 00000000000..4604eafb083 --- /dev/null +++ b/ckan/templates/user/dashboard_groups.html @@ -0,0 +1,23 @@ +{% extends "user/dashboard.html" %} + +{% block dashboard_activity_stream_context %}{% endblock %} + +{% block add_action_content %} + {% if h.check_access('group_create') %} +
  • {% link_for _('Add Group'), controller='group', action='new', class_="btn btn-primary", icon="plus-sign-alt" %}
  • + {% endif %} +{% endblock %} + +{% block primary_content_inner %} +

    {{ _('My Groups') }}

    + {% if c.groups %} + {% snippet "group/snippets/group_list.html", groups=c.groups %} + {% else %} +

    + {{ _('You are not a member of any groups.') }} + {% if h.check_access('group_create') %} + {% link_for _('Create one now?'), controller='group', action='new' %} + {% endif %} +

    + {% endif %} +{% endblock %} diff --git a/ckan/templates/user/dashboard_organizations.html b/ckan/templates/user/dashboard_organizations.html new file mode 100644 index 00000000000..9f96a392397 --- /dev/null +++ b/ckan/templates/user/dashboard_organizations.html @@ -0,0 +1,23 @@ +{% extends "user/dashboard.html" %} + +{% block dashboard_activity_stream_context %}{% endblock %} + +{% block add_action_content %} + {% if h.check_access('organization_create') %} +
  • {% link_for _('Add Organization'), controller='organization', action='new', class_="btn btn-primary", icon="plus-sign-alt" %}
  • + {% endif %} +{% endblock %} + +{% block primary_content_inner %} +

    {{ _('My Organizations') }}

    + {% if c.organizations %} + {% snippet "organization/snippets/organization_list.html", organizations=c.organizations %} + {% else %} +

    + {{ _('You are not a member of any organizations.') }} + {% if h.check_access('organization_create') %} + {% link_for _('Create one now?'), controller='organization', action='new' %} + {% endif %} +

    + {% endif %} +{% endblock %} From ec87d91cb6bd2013337e6c527410d2bd650820ec Mon Sep 17 00:00:00 2001 From: John Martin Date: Wed, 29 May 2013 11:46:09 +0100 Subject: [PATCH 015/175] [#506] Tidys up views to have better breadcrumbs for editin user --- ckan/templates/user/edit.html | 13 +++++++++++-- ckan/templates/user/edit_base.html | 14 -------------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/ckan/templates/user/edit.html b/ckan/templates/user/edit.html index 34203dba58a..7246dbc4e49 100644 --- a/ckan/templates/user/edit.html +++ b/ckan/templates/user/edit.html @@ -1,7 +1,16 @@ {% extends 'user/edit_base.html' %} -{% block actions_content %} - +{% block actions_content %}{% endblock %} + +{% block breadcrumb_content %} + {% if c.userobj.name == c.user_dict.name %} +
  • {{ _('Dashboard') }}
  • +
  • {{ _('Edit settings') }}
  • + {% else %} +
  • {{ _('Users') }}
  • +
  • {{ c.user_dict.display_name }}
  • +
  • {{ _('Edit') }}
  • + {% endif %} {% endblock %} {% block primary_content_inner %} diff --git a/ckan/templates/user/edit_base.html b/ckan/templates/user/edit_base.html index 4826aada4da..7e29720a60d 100644 --- a/ckan/templates/user/edit_base.html +++ b/ckan/templates/user/edit_base.html @@ -4,22 +4,8 @@ {% block subtitle %}{{ _('Dashboard') }}{% endblock %} -{% block breadcrumb_content %} -
  • {{ _('Dashboard') }}
  • -{% endblock %} - {% block primary_content %}
    - {% block page_header %} - - {% endblock %}
    {% block primary_content_inner %}{% endblock %}
    From 951564c9f18c3e1c769f5ce5ee7705489a189008 Mon Sep 17 00:00:00 2001 From: John Martin Date: Wed, 29 May 2013 17:46:23 +0100 Subject: [PATCH 016/175] [#870] First working version of the new search snippets, needs a little tidy up in places --- ckan/public/base/less/ckan.less | 1 + ckan/public/base/less/dataset.less | 147 ------------------ ckan/public/base/less/search.less | 127 +++++++++++++++ ckan/templates/group/index.html | 7 +- ckan/templates/group/read.html | 14 +- ckan/templates/organization/bulk_process.html | 12 +- ckan/templates/organization/index.html | 7 +- ckan/templates/organization/read.html | 14 +- ckan/templates/package/search.html | 75 ++------- .../package/snippets/search_form.html | 63 -------- ckan/templates/snippets/search_form.html | 80 ++++++++++ 11 files changed, 262 insertions(+), 285 deletions(-) create mode 100644 ckan/public/base/less/search.less delete mode 100644 ckan/templates/package/snippets/search_form.html create mode 100644 ckan/templates/snippets/search_form.html diff --git a/ckan/public/base/less/ckan.less b/ckan/public/base/less/ckan.less index bb30be4356e..f7c108f8734 100644 --- a/ckan/public/base/less/ckan.less +++ b/ckan/public/base/less/ckan.less @@ -6,6 +6,7 @@ @import "nav.less"; @import "forms.less"; @import "dataset.less"; +@import "search.less"; @import "group.less"; @import "toolbar.less"; @import "prose.less"; diff --git a/ckan/public/base/less/dataset.less b/ckan/public/base/less/dataset.less index 0e63f9fff88..29b07d6b8a1 100644 --- a/ckan/public/base/less/dataset.less +++ b/ckan/public/base/less/dataset.less @@ -53,153 +53,6 @@ top: 0; } -.results, -.is-search-title { - margin-bottom: 20px; - padding-bottom: 25px; - border-bottom: 1px dotted @genericBorderColor; -} - -.results strong, -.is-search-title { - display: block; - font-size: 24px; - line-height: 1.3; - color: @layoutBoldColor; - margin-bottom: 10px; -} - -.is-search-title { - margin-bottom: 20px; -} - -// Use a before block to space out the area occupied by the sort select box -// this allows the text content in the strong tag to flow correctly around -// the input. -.results strong:before, -.is-search-title:before { - float: right; - content: " "; - width: 280px; - white-space: pre; -} - -.filter-list { - color: @layoutTextColor; - line-height: 32px; - .pill { - line-height: 21px; - } -} - -.filter-list .extra { - margin-top: 10px; - font-size: 18px; - font-weight: normal; - color: @layoutBoldColor; -} - -.dataset-search { - position: relative; -} - -.search-giant, -.search-normal { - position: relative; -} - -.search-normal { - display: block; - margin-bottom: 0; -} - -.search-giant input { - .box-sizing(border-box); - font-size: 16px; - padding: 14px 10px; - width: 100%; - height: auto; -} - -.search-normal input { - .box-sizing(border-box); - width: 100%; - height: auto; -} - -.search-normal button { - cursor: pointer; - position: absolute; - right: 5px; - top: 50%; - background: transparent; - border: none; - color: #999; - margin-top: -17px; - span { - display: none; - } - &:hover { - color: #000; - } -} - -.search-giant button { - cursor: pointer; - position: absolute; - right: 15px; - top: 50%; - display: block; - border: none; - padding: 0; - margin-top: -17px; - width: 30px; - height: 30px; - background: transparent url("@{imagePath}/icon-search-27x26.png") no-repeat center center; - text-indent: -900em; -} - -.control-order-by { - position: absolute; - bottom: -73px; - right: 0; -} - -.control-order-by label, -.control-order-by select { - display: inline; -} - -.control-order-by select { - width: 160px; -} - -.search-aside { - .control-order-by { - clear: both; - overflow: hidden; - display: block; - position: relative; - bottom: 0; - label { - float: left; - font-weight: normal; - font-size: 12px; - line-height: 20px; - } - select { - float: left; - padding: 2px 4px; - margin: 0; - width: inherit; - font-size: 12px; - height: 20px; - line-height: 20px; - width: 120px; - } - } -} - // Resource List .resource-list { diff --git a/ckan/public/base/less/search.less b/ckan/public/base/less/search.less new file mode 100644 index 00000000000..a41875cf458 --- /dev/null +++ b/ckan/public/base/less/search.less @@ -0,0 +1,127 @@ +.search-form { + // .clearfix; + margin-bottom: 20px; + padding-bottom: 25px; + border-bottom: 1px dotted @genericBorderColor; + + // Normal search box + .search-input { + position: relative; + margin-bottom: 20px; + input { + .box-sizing(border-box); + margin: 0; + width: 100%; + height: auto; + } + button { + cursor: pointer; + display: block; + position: absolute; + top: 50%; + margin-top: -10px; + right: 10px; + height: 20px; + padding: 0; + border: none; + background: transparent; + span { + display: none; + } + i { + color: @inputBorder; + .transition(color 0.2s ease-in); + } + &:hover i { + color: @inputColor; + } + } + &.search-giant { + input { + font-size: 16px; + padding: 15px; + } + button { + margin-top: -15px; + right: 15px; + height: 30px; + i { + font-size: 28px; + width: 28px; + } + } + } + } + .control-order-by { + float: right; + margin: 0 0 0 15px; + label, + select { + display: inline; + } + select { + width: 160px; + margin: 0; + } + } + h2 { + font-size: 24px; + line-height: 1.3; + color: @layoutBoldColor; + margin-bottom: 0; + } + .filter-list { + color: @layoutTextColor; + line-height: 32px; + margin: 10px 0 0 0; + .pill { + line-height: 21px; + } + .extra { + margin-top: 10px; + font-size: 18px; + font-weight: normal; + color: @layoutBoldColor; + } + } +} + +/*.search-giant + +.search-normal input { + .box-sizing(border-box); + width: 100%; + height: auto; +} + +.search-normal button { + cursor: pointer; + position: absolute; + right: 5px; + top: 50%; + background: transparent; + border: none; + color: #999; + margin-top: -17px; + span { + display: none; + } + &:hover { + color: #000; + } +} + +.search-giant button { + cursor: pointer; + position: absolute; + right: 15px; + top: 50%; + display: block; + border: none; + padding: 0; + margin-top: -17px; + width: 30px; + height: 30px; + background: transparent url("@{imagePath}/icon-search-27x26.png") no-repeat center center; + text-indent: -900em; +}*/ diff --git a/ckan/templates/group/index.html b/ckan/templates/group/index.html index 5c137aea788..50376596bd1 100644 --- a/ckan/templates/group/index.html +++ b/ckan/templates/group/index.html @@ -17,13 +17,8 @@

    {{ _('Groups') }}

    {% block groups_search_form %} - {% snippet 'snippets/simple_search.html', q=c.q, sort=c.sort_by_selected, placeholder=_('Search groups...') %} + {% snippet 'snippets/search_form.html', type='group', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, placeholder=_('Search groups...'), show_empty=request.params %} {% endblock %} -

    - {% block groups_search_result_text %} - {% snippet 'snippets/search_result_text.html', query=c.q, count=c.page.item_count, type='group' %} - {% endblock %} -

    {% block groups_list %} {% if c.page.items or request.params %} {% snippet "group/snippets/group_list.html", groups=c.page.items %} diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html index f56359b37d9..bf7ca656fa5 100644 --- a/ckan/templates/group/read.html +++ b/ckan/templates/group/read.html @@ -4,8 +4,20 @@ {% block primary_content_inner %}
    + {% block groups_search_form %} + {% set facets = { + 'fields': c.fields_grouped, + 'search': c.search_facets, + 'titles': c.facet_titles, + 'translated_fields': c.translated_fields, + 'remove_field': c.remove_field } + %} + {% snippet 'snippets/search_form.html', type='dataset', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, facets=facets, placeholder=_('Search datasets...'), show_empty=request.params %} + {% endblock %} {% block packages_list %} - {% include "package/snippets/search_form.html" %} + {% if c.page.items %} + {{ h.snippet('snippets/package_list.html', packages=c.page.items) }} + {% endif %} {% endblock %}
    {% block page_pagination %} diff --git a/ckan/templates/organization/bulk_process.html b/ckan/templates/organization/bulk_process.html index df45b0ebf35..ec51755f013 100644 --- a/ckan/templates/organization/bulk_process.html +++ b/ckan/templates/organization/bulk_process.html @@ -90,7 +90,17 @@