Feature: Add API to retrieve the list of datasets acquired by a user

This commit is contained in:
Aitor Magán 2015-01-23 14:44:40 +01:00
parent 5b8c87f605
commit e9a5a50653
9 changed files with 203 additions and 91 deletions

View File

@ -19,6 +19,7 @@
import ckan.plugins as plugins import ckan.plugins as plugins
import ckanext.privatedatasets.constants as constants import ckanext.privatedatasets.constants as constants
import db
import importlib import importlib
import logging import logging
import pylons.config as config import pylons.config as config
@ -29,6 +30,28 @@ PARSER_CONFIG_PROP = 'ckan.privatedatasets.parser'
def package_acquired(context, request_data): 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) 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 # Return warnings that inform about non-existing datasets
if len(warns) > 0: if len(warns) > 0:
return {'warns': warns} 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

View File

@ -133,3 +133,7 @@ def resource_show(context, data_dict):
def package_acquired(context, data_dict): def package_acquired(context, data_dict):
# TODO: Improve security # TODO: Improve security
return {'success': True} return {'success': True}
def acquisitions_list(context, data_dict):
# Users can get only their acquisitions list
return {'success': context['user'] == data_dict['user']}

View File

@ -18,6 +18,7 @@
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>. # along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
ALLOWED_USERS = 'allowed_users' ALLOWED_USERS = 'allowed_users'
ACQUISITIONS_LIST = 'acquisitions_list'
ALLOWED_USERS_STR = 'allowed_users_str' ALLOWED_USERS_STR = 'allowed_users_str'
SEARCHABLE = 'searchable' SEARCHABLE = 'searchable'
ACQUIRE_URL = 'acquire_url' ACQUIRE_URL = 'acquire_url'

View File

@ -17,10 +17,10 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>. # along with CKAN Private Dataset Extension. If not, see <http://www.gnu.org/licenses/>.
import ckanext.privatedatasets.constants as constants
import ckan.lib.base as base import ckan.lib.base as base
import ckan.model as model import ckan.model as model
import ckan.plugins as plugins import ckan.plugins as plugins
import ckanext.privatedatasets.db as db
import logging import logging
from ckan.common import _ from ckan.common import _
@ -32,8 +32,6 @@ class AcquiredDatasetsControllerUI(base.BaseController):
def user_acquired_datasets(self): def user_acquired_datasets(self):
db.init_db(model)
c = plugins.toolkit.c c = plugins.toolkit.c
context = { context = {
'model': model, 'model': model,
@ -44,23 +42,10 @@ class AcquiredDatasetsControllerUI(base.BaseController):
# Get user information # Get user information
try: try:
c.user_dict = plugins.toolkit.get_action('user_show')(context.copy(), {'user_obj': c.userobj}) 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: except plugins.toolkit.ObjectNotFound:
plugins.toolkit.abort(404, _('User not found')) plugins.toolkit.abort(404, _('User not found'))
except plugins.toolkit.NotAuthorized: except plugins.toolkit.NotAuthorized:
plugins.toolkit.abort(401, _('Not authorized to see this page')) 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') return plugins.toolkit.render('user/dashboard_acquired.html')

View File

@ -107,7 +107,8 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
return {'package_show': auth.package_show, return {'package_show': auth.package_show,
'package_update': auth.package_update, 'package_update': auth.package_update,
'resource_show': auth.resource_show, 'resource_show': auth.resource_show,
constants.PACKAGE_ACQUIRED: auth.package_acquired} constants.PACKAGE_ACQUIRED: auth.package_acquired,
constants.ACQUISITIONS_LIST: auth.acquisitions_list}
###################################################################### ######################################################################
############################ ICONFIGURER ############################# ############################ ICONFIGURER #############################
@ -138,7 +139,10 @@ class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm):
###################################################################### ######################################################################
def get_actions(self): 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 ######################### ######################### IPACKAGECONTROLLER #########################

View File

@ -43,11 +43,15 @@ class ActionsTest(unittest.TestCase):
self._plugins = actions.plugins self._plugins = actions.plugins
actions.plugins = MagicMock() actions.plugins = MagicMock()
self._db = actions.db
actions.db = MagicMock()
def tearDown(self): def tearDown(self):
# Unmock # Unmock
actions.config = self._config actions.config = self._config
actions.importlib = self._importlib actions.importlib = self._importlib
actions.plugins = self._plugins actions.plugins = self._plugins
actions.db = self._db
@parameterized.expand([ @parameterized.expand([
('', None, False, False, '%s not configured' % PARSER_CONFIG_PROP), ('', None, False, False, '%s not configured' % PARSER_CONFIG_PROP),
@ -201,3 +205,82 @@ class ActionsTest(unittest.TestCase):
context_update['ignore_auth'] = True context_update['ignore_auth'] = True
package_update.assert_any_call(context_update, {'id': dataset_id, 'allowed_users': expected_allowed_users, 'private': 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)

View File

@ -249,4 +249,12 @@ class AuthTest(unittest.TestCase):
self.assertEquals(authorized_pkg, result['success']) self.assertEquals(authorized_pkg, result['success'])
def test_package_acquired(self): 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'])

View File

@ -38,9 +38,6 @@ class UIControllerTest(unittest.TestCase):
self._model = controller.model self._model = controller.model
controller.model = MagicMock() controller.model = MagicMock()
self._db = controller.db
controller.db = MagicMock()
# Set exceptions # Set exceptions
controller.plugins.toolkit.ObjectNotFound = self._plugins.toolkit.ObjectNotFound controller.plugins.toolkit.ObjectNotFound = self._plugins.toolkit.ObjectNotFound
controller.plugins.toolkit.NotAuthorized = self._plugins.toolkit.NotAuthorized controller.plugins.toolkit.NotAuthorized = self._plugins.toolkit.NotAuthorized
@ -49,7 +46,6 @@ class UIControllerTest(unittest.TestCase):
# Unmock # Unmock
controller.plugins = self._plugins controller.plugins = self._plugins
controller.model = self._model controller.model = self._model
controller.db = self._db
@parameterized.expand([ @parameterized.expand([
(controller.plugins.toolkit.ObjectNotFound, 404), (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}) 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) controller.plugins.toolkit.abort.assert_called_once_with(expected_status, ANY)
@parameterized.expand([ def test_no_error_loading_users(self):
({},),
({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=[]):
pkgs_ids = [0, 1, 2, 3]
user = 'example_user_test' user = 'example_user_test'
controller.plugins.toolkit.c.user = user controller.plugins.toolkit.c.user = user
# get_action mock # actions
default_package = {'pkg_id': 0, 'test': 'ok', 'res': 'ta'} default_user = {'user_name': 'test', 'another_val': 'example value'}
user_show = MagicMock(return_value=default_user)
def _package_show(context, data_dict): acquisitions_list = MagicMock()
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())
def _get_action(action): def _get_action(action):
if action == 'package_show': if action == 'user_show':
return package_show
elif action == 'user_show':
return user_show return user_show
else:
return acquisitions_list
controller.plugins.toolkit.get_action = MagicMock(side_effect=_get_action) 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 # Call the function
returned = self.instanceUI.user_acquired_datasets() 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 # User_show called correctly
expected_context = { expected_context = {
'model': controller.model, '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}) user_show.assert_called_once_with(expected_context, {'user_obj': controller.plugins.toolkit.c.userobj})
# Query called correctry # Query called correctry
controller.db.AllowedUser.get.assert_called_once_with(user_name=user) expected_user = default_user.copy()
expected_user['acquired_datasets'] = acquisitions_list.return_value
# Assert that the package_show has been called properly acquisitions_list.assert_called_with(expected_context, None)
self.assertEquals(len(pkgs_ids), package_show.call_count) self.assertEquals(expected_user, controller.plugins.toolkit.c.user_dict)
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)
# Check that the render method has been called and that its result has been returned # Check that the render method has been called and that its result has been returned
self.assertEquals(controller.plugins.toolkit.render.return_value, returned) self.assertEquals(controller.plugins.toolkit.render.return_value, returned)

View File

@ -58,10 +58,11 @@ class PluginTest(unittest.TestCase):
self.assertTrue(interface.implemented_by(plugin.PrivateDatasets)) self.assertTrue(interface.implemented_by(plugin.PrivateDatasets))
@parameterized.expand([ @parameterized.expand([
('package_show', plugin.auth.package_show), ('package_show', plugin.auth.package_show),
('package_update', plugin.auth.package_update), ('package_update', plugin.auth.package_update),
('package_show', plugin.auth.package_show), ('package_show', plugin.auth.package_show),
('package_acquired', plugin.auth.package_acquired) ('package_acquired', plugin.auth.package_acquired),
('acquisitions_list', plugin.auth.acquisitions_list)
]) ])
def test_auth_function(self, function_name, expected_function): def test_auth_function(self, function_name, expected_function):
auth_functions = self.privateDatasets.get_auth_functions() auth_functions = self.privateDatasets.get_auth_functions()
@ -87,7 +88,8 @@ class PluginTest(unittest.TestCase):
action='user_acquired_datasets', conditions=dict(method=['GET'])) action='user_acquired_datasets', conditions=dict(method=['GET']))
@parameterized.expand([ @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): def test_actions_function(self, function_name, expected_function):
actions = self.privateDatasets.get_actions() actions = self.privateDatasets.get_actions()