diff --git a/ckanext/privatedatasets/actions.py b/ckanext/privatedatasets/actions.py index 40582bb..1fdf8bf 100644 --- a/ckanext/privatedatasets/actions.py +++ b/ckanext/privatedatasets/actions.py @@ -19,6 +19,7 @@ import ckan.plugins as plugins import ckanext.privatedatasets.constants as constants +import db import importlib import logging import pylons.config as config @@ -29,6 +30,28 @@ PARSER_CONFIG_PROP = 'ckan.privatedatasets.parser' def package_acquired(context, request_data): + ''' + API action to be called every time a user acquires a dataset in an external service. + + This API should be called to add the user to the list of allowed users. + + Since each service can provide a different way of pushing the data, the received + data will be forwarded to the parser set in the preferences. This parser should + return a dict similar to the following one: + {'errors': ["...", "...", ...] + 'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]} + 1) 'errors' contains the list of errors. It should be empty if no errors arised + while the notification is parsed + 2) 'users_datasets' is the lists of datasets available for each user (each element + of this list is a dictionary with two fields: user and datasets). + + :parameter request_data: Depends on the parser + :type request_data: dict + + :return: A list of warnings or None if the list of warnings is empty + :rtype: dict + + ''' log.info('Notification received: %s' % request_data) @@ -107,3 +130,59 @@ def package_acquired(context, request_data): # Return warnings that inform about non-existing datasets if len(warns) > 0: return {'warns': warns} + +def acquisitions_list(context, data_dict): + ''' + API to retrieve the list of datasets that have been acquired by a certain user + + :parameter user: The user whose acquired dataset you want to retrieve. This parameter + is optional. If you don't include this identifier, the system will use the one + of the user that is performing the request + :type user: string + + :return: The list of datarequest that has been acquired by the specified user + :rtype: list + ''' + + if data_dict is None: + data_dict = {} + + if 'user' not in data_dict and 'user' in context: + data_dict['user'] = context['user'] + + plugins.toolkit.check_access(constants.ACQUISITIONS_LIST, context.copy(), data_dict) + + # Init db + db.init_db(context['model']) + + # Init the result array + result = [] + + # Check that the user exists + try: + plugins.toolkit.get_validator('user_name_exists')(data_dict['user'], context.copy()) + except Exception: + raise plugins.toolkit.ValidationError('User %s does not exist' % data_dict['user']) + + # Get the datasets acquired by the user + query = db.AllowedUser.get(user_name=data_dict['user']) + + # Get the datasets + for dataset in query: + try: + dataset_show_func = 'dataset_show' + func_data_dict = {'id': dataset.package_id} + internal_context = context.copy() + + # Check that the the dataset can be accessed and get its data + # FIX: If the check_access function is not called, an exception is risen. + plugins.toolkit.check_access(dataset_show_func, internal_context, func_data_dict) + dataset_dict = plugins.toolkit.get_action(dataset_show_func)(internal_context, func_data_dict) + + # Only packages with state == 'active' can be shown + if dataset_dict.get('state', None) == 'active': + result.append(dataset_dict) + except Exception: + pass + + return result diff --git a/ckanext/privatedatasets/auth.py b/ckanext/privatedatasets/auth.py index c1fb0e4..186221e 100644 --- a/ckanext/privatedatasets/auth.py +++ b/ckanext/privatedatasets/auth.py @@ -133,3 +133,7 @@ def resource_show(context, data_dict): def package_acquired(context, data_dict): # TODO: Improve security return {'success': True} + +def acquisitions_list(context, data_dict): + # Users can get only their acquisitions list + return {'success': context['user'] == data_dict['user']} diff --git a/ckanext/privatedatasets/constants.py b/ckanext/privatedatasets/constants.py index bf47f81..8f00449 100644 --- a/ckanext/privatedatasets/constants.py +++ b/ckanext/privatedatasets/constants.py @@ -18,6 +18,7 @@ # along with CKAN Private Dataset Extension. If not, see . ALLOWED_USERS = 'allowed_users' +ACQUISITIONS_LIST = 'acquisitions_list' ALLOWED_USERS_STR = 'allowed_users_str' SEARCHABLE = 'searchable' ACQUIRE_URL = 'acquire_url' diff --git a/ckanext/privatedatasets/controllers/ui_controller.py b/ckanext/privatedatasets/controllers/ui_controller.py index 336ea07..573ae25 100644 --- a/ckanext/privatedatasets/controllers/ui_controller.py +++ b/ckanext/privatedatasets/controllers/ui_controller.py @@ -17,10 +17,10 @@ # You should have received a copy of the GNU Affero General Public License # along with CKAN Private Dataset Extension. If not, see . +import ckanext.privatedatasets.constants as constants import ckan.lib.base as base import ckan.model as model import ckan.plugins as plugins -import ckanext.privatedatasets.db as db import logging from ckan.common import _ @@ -32,8 +32,6 @@ class AcquiredDatasetsControllerUI(base.BaseController): def user_acquired_datasets(self): - db.init_db(model) - c = plugins.toolkit.c context = { 'model': model, @@ -44,23 +42,10 @@ class AcquiredDatasetsControllerUI(base.BaseController): # Get user information try: c.user_dict = plugins.toolkit.get_action('user_show')(context.copy(), {'user_obj': c.userobj}) - c.user_dict['acquired_datasets'] = [] + c.user_dict['acquired_datasets'] = plugins.toolkit.get_action(constants.ACQUISITIONS_LIST)(context.copy(), None) except plugins.toolkit.ObjectNotFound: plugins.toolkit.abort(404, _('User not found')) except plugins.toolkit.NotAuthorized: plugins.toolkit.abort(401, _('Not authorized to see this page')) - # Get the datasets acquired by the user - query = db.AllowedUser.get(user_name=context['user']) - - # Get the datasets - for dataset in query: - try: - dataset_dict = plugins.toolkit.get_action('package_show')(context.copy(), {'id': dataset.package_id}) - # Only packages with state == 'active' can be shown - if dataset_dict.get('state', None) == 'active': - c.user_dict['acquired_datasets'].append(dataset_dict) - except Exception: - continue - return plugins.toolkit.render('user/dashboard_acquired.html') diff --git a/ckanext/privatedatasets/plugin.py b/ckanext/privatedatasets/plugin.py index 714a2dc..3bd8952 100644 --- a/ckanext/privatedatasets/plugin.py +++ b/ckanext/privatedatasets/plugin.py @@ -107,7 +107,8 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm): return {'package_show': auth.package_show, 'package_update': auth.package_update, 'resource_show': auth.resource_show, - constants.PACKAGE_ACQUIRED: auth.package_acquired} + constants.PACKAGE_ACQUIRED: auth.package_acquired, + constants.ACQUISITIONS_LIST: auth.acquisitions_list} ###################################################################### ############################ ICONFIGURER ############################# @@ -138,7 +139,10 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm): ###################################################################### def get_actions(self): - return {constants.PACKAGE_ACQUIRED: actions.package_acquired} + return { + constants.PACKAGE_ACQUIRED: actions.package_acquired, + constants.ACQUISITIONS_LIST: actions.acquisitions_list + } ###################################################################### ######################### IPACKAGECONTROLLER ######################### diff --git a/ckanext/privatedatasets/tests/test_actions.py b/ckanext/privatedatasets/tests/test_actions.py index f7f4523..bfb36cc 100644 --- a/ckanext/privatedatasets/tests/test_actions.py +++ b/ckanext/privatedatasets/tests/test_actions.py @@ -43,11 +43,15 @@ class ActionsTest(unittest.TestCase): self._plugins = actions.plugins actions.plugins = MagicMock() + self._db = actions.db + actions.db = MagicMock() + def tearDown(self): # Unmock actions.config = self._config actions.importlib = self._importlib actions.plugins = self._plugins + actions.db = self._db @parameterized.expand([ ('', None, False, False, '%s not configured' % PARSER_CONFIG_PROP), @@ -201,3 +205,82 @@ class ActionsTest(unittest.TestCase): context_update['ignore_auth'] = True package_update.assert_any_call(context_update, {'id': dataset_id, 'allowed_users': expected_allowed_users, 'private': True}) + + + @parameterized.expand([ + (None, {},), + ({}, {2: actions.plugins.toolkit.ObjectNotFound},), + ({'user': 'fiware'}, {1: actions.plugins.toolkit.NotAuthorized},), + (None, {1: actions.plugins.toolkit.NotAuthorized, 2: actions.plugins.toolkit.ObjectNotFound},), + ({}, {}, [1]), + ({'user': 'fiware'}, {}, [3, 2]), + (None, {1: actions.plugins.toolkit.NotAuthorized}, [2]), + ({}, {1: actions.plugins.toolkit.NotAuthorized, 2: actions.plugins.toolkit.ObjectNotFound}, [1, 3]), + ]) + def test_acquisitions_list(self, data_dict, package_errors={}, deleted_packages=[]): + + pkgs_ids = [0, 1, 2, 3] + user = 'example_user_test' + actions.plugins.toolkit.c.user = user + + # get_action mock + default_package = {'pkg_id': 0, 'test': 'ok', 'res': 'ta'} + + def _package_show(context, data_dict): + if data_dict['id'] in package_errors: + raise package_errors[data_dict['id']]('ERROR') + else: + pkg = default_package.copy() + pkg['pkg_id'] = data_dict['id'] + pkg['state'] = 'deleted' if data_dict['id'] in deleted_packages else 'active' + return pkg + + package_show = MagicMock(side_effect=_package_show) + actions.plugins.toolkit.get_action.return_value = package_show + + # query mock + query_res = [] + for i in pkgs_ids: + pkg = MagicMock() + pkg.package_id = i + pkg.user_name = user + query_res.append(pkg) + + actions.db.AllowedUser.get = MagicMock(return_value=query_res) + + # Context + context = { + 'model': MagicMock(), + 'user': 'default_user' + } + + # Call the function + result = actions.acquisitions_list(context, data_dict) + + # Asset that check_access has been called + actions.plugins.toolkit.chec_access(actions.constants.ACQUISITIONS_LIST, context, data_dict) + + # Check that the database has been initialized properly + actions.db.init_db.assert_called_once_with(context['model']) + + # Set expected user + expected_user = data_dict['user'] if data_dict is not None and 'user' in data_dict else context['user'] + + # Query called correctry + actions.db.AllowedUser.get.assert_called_once_with(user_name=expected_user) + + # Assert that the package_show has been called properly + self.assertEquals(len(pkgs_ids), package_show.call_count) + for i in pkgs_ids: + package_show.assert_any_call(context, {'id': i}) + + # Check that the template receives the correct datasets + expected_acquired_datasets = [] + for i in pkgs_ids: + if i not in package_errors and i not in deleted_packages: + pkg = default_package.copy() + pkg['pkg_id'] = i + pkg['state'] = 'deleted' if i in deleted_packages else 'active' + expected_acquired_datasets.append(pkg) + + self.assertEquals(expected_acquired_datasets, result) diff --git a/ckanext/privatedatasets/tests/test_auth.py b/ckanext/privatedatasets/tests/test_auth.py index 19d32d1..d7c1978 100644 --- a/ckanext/privatedatasets/tests/test_auth.py +++ b/ckanext/privatedatasets/tests/test_auth.py @@ -249,4 +249,12 @@ class AuthTest(unittest.TestCase): self.assertEquals(authorized_pkg, result['success']) def test_package_acquired(self): - self.assertTrue(auth.package_acquired({}, {})) + self.assertTrue(auth.package_acquired({}, {})['success']) + + @parameterized.expand([ + ({'user': 'user_1'}, {'user': 'user_1'}, True), + ({'user': 'user_2'}, {'user': 'user_1'}, False), + ]) + def test_acquisitions_list(self, context, data_dict, expected_result): + self.assertEquals(expected_result, auth.acquisitions_list(context, data_dict)['success']) + diff --git a/ckanext/privatedatasets/tests/test_controller_ui.py b/ckanext/privatedatasets/tests/test_controller_ui.py index b5c61d6..6b2cc1e 100644 --- a/ckanext/privatedatasets/tests/test_controller_ui.py +++ b/ckanext/privatedatasets/tests/test_controller_ui.py @@ -38,9 +38,6 @@ class UIControllerTest(unittest.TestCase): self._model = controller.model controller.model = MagicMock() - self._db = controller.db - controller.db = MagicMock() - # Set exceptions controller.plugins.toolkit.ObjectNotFound = self._plugins.toolkit.ObjectNotFound controller.plugins.toolkit.NotAuthorized = self._plugins.toolkit.NotAuthorized @@ -49,7 +46,6 @@ class UIControllerTest(unittest.TestCase): # Unmock controller.plugins = self._plugins controller.model = self._model - controller.db = self._db @parameterized.expand([ (controller.plugins.toolkit.ObjectNotFound, 404), @@ -74,62 +70,26 @@ class UIControllerTest(unittest.TestCase): user_show.assert_called_once_with(expected_context, {'user_obj': controller.plugins.toolkit.c.userobj}) controller.plugins.toolkit.abort.assert_called_once_with(expected_status, ANY) - @parameterized.expand([ - ({},), - ({2: controller.plugins.toolkit.ObjectNotFound},), - ({1: controller.plugins.toolkit.NotAuthorized},), - ({1: controller.plugins.toolkit.NotAuthorized, 2: controller.plugins.toolkit.ObjectNotFound},), - ({}, [1]), - ({}, [3, 2]), - ({1: controller.plugins.toolkit.NotAuthorized}, [2]), - ({1: controller.plugins.toolkit.NotAuthorized, 2: controller.plugins.toolkit.ObjectNotFound}, [1, 3]), - ]) - def test_no_error_loading_users(self, package_errors={}, deleted_packages=[]): + def test_no_error_loading_users(self): - pkgs_ids = [0, 1, 2, 3] user = 'example_user_test' controller.plugins.toolkit.c.user = user - # get_action mock - default_package = {'pkg_id': 0, 'test': 'ok', 'res': 'ta'} - - def _package_show(context, data_dict): - if data_dict['id'] in package_errors: - raise package_errors[data_dict['id']]('ERROR') - else: - pkg = default_package.copy() - pkg['pkg_id'] = data_dict['id'] - pkg['state'] = 'deleted' if data_dict['id'] in deleted_packages else 'active' - return pkg - - user_dict = {'user_name': 'test', 'another_val': 'example value'} - package_show = MagicMock(side_effect=_package_show) - user_show = MagicMock(return_value=user_dict.copy()) - + # actions + default_user = {'user_name': 'test', 'another_val': 'example value'} + user_show = MagicMock(return_value=default_user) + acquisitions_list = MagicMock() def _get_action(action): - if action == 'package_show': - return package_show - elif action == 'user_show': + if action == 'user_show': return user_show + else: + return acquisitions_list controller.plugins.toolkit.get_action = MagicMock(side_effect=_get_action) - # query mock - query_res = [] - for i in pkgs_ids: - pkg = MagicMock() - pkg.package_id = i - pkg.user_name = user - query_res.append(pkg) - - controller.db.AllowedUser.get = MagicMock(return_value=query_res) - # Call the function returned = self.instanceUI.user_acquired_datasets() - # Check that the database has been initialized properly - controller.db.init_db.assert_called_once_with(controller.model) - # User_show called correctly expected_context = { 'model': controller.model, @@ -140,24 +100,10 @@ class UIControllerTest(unittest.TestCase): user_show.assert_called_once_with(expected_context, {'user_obj': controller.plugins.toolkit.c.userobj}) # Query called correctry - controller.db.AllowedUser.get.assert_called_once_with(user_name=user) - - # Assert that the package_show has been called properly - self.assertEquals(len(pkgs_ids), package_show.call_count) - for i in pkgs_ids: - package_show.assert_any_call(expected_context, {'id': i}) - - # Check that the template receives the correct datasets - expected_user_dict = user_dict.copy() - expected_user_dict['acquired_datasets'] = [] - for i in pkgs_ids: - if i not in package_errors and i not in deleted_packages: - pkg = default_package.copy() - pkg['pkg_id'] = i - pkg['state'] = 'deleted' if i in deleted_packages else 'active' - expected_user_dict['acquired_datasets'].append(pkg) - - self.assertEquals(expected_user_dict, controller.plugins.toolkit.c.user_dict) + expected_user = default_user.copy() + expected_user['acquired_datasets'] = acquisitions_list.return_value + acquisitions_list.assert_called_with(expected_context, None) + self.assertEquals(expected_user, controller.plugins.toolkit.c.user_dict) # Check that the render method has been called and that its result has been returned self.assertEquals(controller.plugins.toolkit.render.return_value, returned) diff --git a/ckanext/privatedatasets/tests/test_plugin.py b/ckanext/privatedatasets/tests/test_plugin.py index b32f696..fd07cbc 100644 --- a/ckanext/privatedatasets/tests/test_plugin.py +++ b/ckanext/privatedatasets/tests/test_plugin.py @@ -58,10 +58,11 @@ class PluginTest(unittest.TestCase): self.assertTrue(interface.implemented_by(plugin.PrivateDatasets)) @parameterized.expand([ - ('package_show', plugin.auth.package_show), - ('package_update', plugin.auth.package_update), - ('package_show', plugin.auth.package_show), - ('package_acquired', plugin.auth.package_acquired) + ('package_show', plugin.auth.package_show), + ('package_update', plugin.auth.package_update), + ('package_show', plugin.auth.package_show), + ('package_acquired', plugin.auth.package_acquired), + ('acquisitions_list', plugin.auth.acquisitions_list) ]) def test_auth_function(self, function_name, expected_function): auth_functions = self.privateDatasets.get_auth_functions() @@ -87,7 +88,8 @@ class PluginTest(unittest.TestCase): action='user_acquired_datasets', conditions=dict(method=['GET'])) @parameterized.expand([ - ('package_acquired', plugin.actions.package_acquired) + ('package_acquired', plugin.actions.package_acquired), + ('acquisitions_list', plugin.actions.acquisitions_list) ]) def test_actions_function(self, function_name, expected_function): actions = self.privateDatasets.get_actions()