diff --git a/ckanext/ed/actions.py b/ckanext/ed/actions.py
index d0cb1ef..4cb3964 100644
--- a/ckanext/ed/actions.py
+++ b/ckanext/ed/actions.py
@@ -1,12 +1,14 @@
-from ckan.plugins import toolkit
+from logging import getLogger
+import os
+import requests
import uuid
-from ckanext.ed import helpers
import zipfile
-import os
+
from ckan.controllers.admin import get_sysadmins
-import requests
-from logging import getLogger
+from ckan.logic.action.get import package_show as core_package_show
+from ckan.plugins import toolkit
+from ckanext.ed import helpers
SUPPORTED_RESOURCE_MIMETYPES = [
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
@@ -136,3 +138,18 @@ def prepare_zip_resources(context, data_dict):
os.remove(file_path)
return {'zip_id': None}
+
+
+@toolkit.side_effect_free
+def package_show(context, data_dict):
+ package = core_package_show(context, data_dict)
+ # User with less perms then creator should not be able to access pending dataset
+ approval_pending = package.get('approval_state') == 'approval_pending'
+ try:
+ toolkit.check_access('package_update', context, data_dict)
+ can_edit = True
+ except toolkit.NotAuthorized:
+ can_edit = False
+ if not can_edit and approval_pending:
+ raise toolkit.ObjectNotFound
+ return package
diff --git a/ckanext/ed/helpers.py b/ckanext/ed/helpers.py
index 0193dc1..d6bffdb 100644
--- a/ckanext/ed/helpers.py
+++ b/ckanext/ed/helpers.py
@@ -29,38 +29,39 @@ def get_recently_updated_datasets(limit=5):
Returns recent created or updated datasets.
:param limit: Limit of the datasets to be returned. Default is 5.
:type limit: integer
+ :param user: user name
+ :type user: string
+
:returns: a list of recently created or updated datasets
:rtype: list
'''
try:
- pkg_search_results = toolkit.get_action('package_search')(data_dict={
- 'sort': 'metadata_modified desc',
- 'rows': limit,
- })['results']
+ pkg_search_results = toolkit.get_action('package_search')(
+ data_dict={
+ 'sort': 'metadata_modified desc',
+ 'rows': limit
+ })['results']
+ return pkg_search_results
except toolkit.ValidationError, search.SearchError:
return []
else:
- pkgs = []
- for pkg in pkg_search_results:
- package = toolkit.get_action('package_show')(
- data_dict={'id': pkg['id']})
- modified = datetime.strptime(
- package['metadata_modified'].split('T')[0], '%Y-%m-%d')
- package['days_ago_modified'] = ((datetime.now() - modified).days)
- pkgs.append(package)
- return pkgs
-
+ log.warning('Unexpected Error occured while searching')
+ return []
def get_most_popular_datasets(limit=5):
'''
Returns most popular datasets based on total views.
:param limit: Limit of the datasets to be returned. Default is 5.
:type limit: integer
+ :param user: user name
+ :type user: string
+
:returns: a list of most popular datasets
:rtype: list
'''
- data = pkg_search_results = toolkit.get_action('package_search')(data_dict={
+ data = pkg_search_results = toolkit.get_action('package_search')(
+ data_dict={
'sort': 'views_total desc',
'rows': limit,
})['results']
@@ -105,3 +106,17 @@ def get_total_views_for_dataset(id):
return dataset.get('tracking_summary').get('total')
except Exception:
return 0
+
+
+def is_admin(user):
+ """
+ Returns True if user is admin of any organisation
+
+ :param user: user name
+ :type user: string
+
+ :returns: True/False
+ :rtype: boolean
+ """
+ user_orgs = _get_action('organization_list_for_user', {'user': user}, {'user': user})
+ return any([i.get('capacity') == 'admin' for i in user_orgs])
diff --git a/ckanext/ed/plugin.py b/ckanext/ed/plugin.py
index 5fb27f0..d5c8ba1 100644
--- a/ckanext/ed/plugin.py
+++ b/ckanext/ed/plugin.py
@@ -1,8 +1,8 @@
+from ckan.lib.plugins import DefaultTranslation
import ckan.plugins as plugins
import ckan.plugins.toolkit as toolkit
-from ckanext.ed import helpers
-from ckanext.ed import actions
-from ckan.lib.plugins import DefaultTranslation
+
+from ckanext.ed import actions, helpers, validators
class EDPlugin(plugins.SingletonPlugin, DefaultTranslation):
@@ -11,33 +11,40 @@ class EDPlugin(plugins.SingletonPlugin, DefaultTranslation):
plugins.implements(plugins.ITranslation)
plugins.implements(plugins.IActions)
plugins.implements(plugins.IRoutes, inherit=True)
+ plugins.implements(plugins.IValidators)
+ plugins.implements(plugins.IPackageController, inherit=True)
# ITemplateHelpers
-
def get_helpers(self):
return {
'ed_get_groups': helpers.get_groups,
+ 'ed_is_admin': helpers.is_admin,
'ed_get_recently_updated_datasets': helpers.get_recently_updated_datasets,
'ed_get_most_popular_datasets': helpers.get_most_popular_datasets,
'ed_get_total_views_for_dataset': helpers.get_total_views_for_dataset,
}
# IActions
-
def get_actions(self):
return {
'ed_prepare_zip_resources': actions.prepare_zip_resources,
+ 'package_show': actions.package_show
}
- # IConfigurer
+ # IPackageController
+ def before_search(self, search_params):
+ search_params.update({
+ 'fq': '!(approval_state:approval_pending) ' + search_params.get('fq', '')
+ })
+ return search_params
+ # IConfigurer
def update_config(self, config_):
toolkit.add_template_directory(config_, 'templates')
toolkit.add_public_directory(config_, 'public')
toolkit.add_resource('fanstatic', 'ed')
# IRoutes
-
def before_map(self, map):
map.connect(
'download_zip',
@@ -47,3 +54,9 @@ def before_map(self, map):
)
return map
+
+ # IValidators
+ def get_validators(self):
+ return {
+ 'state_validator': validators.state_validator
+ }
diff --git a/ckanext/ed/schemas/dataset.json b/ckanext/ed/schemas/dataset.json
index b7661b8..6796945 100644
--- a/ckanext/ed/schemas/dataset.json
+++ b/ckanext/ed/schemas/dataset.json
@@ -117,6 +117,11 @@
"label": "Temporal",
"form_placeholder": "eg. 2000-01-15T00:45:00Z/2010-01-15T00:06:00Z",
"help_text": "The range of temporal applicability of a dataset (i.e., a start and end date of applicability for the data)."
+ },
+ {
+ "field_name": "approval_state",
+ "form_snippet": null,
+ "validators": "state_validator"
}
],
"resource_fields": [
diff --git a/ckanext/ed/templates/package/new_package_form.html b/ckanext/ed/templates/package/new_package_form.html
new file mode 100644
index 0000000..da5b87d
--- /dev/null
+++ b/ckanext/ed/templates/package/new_package_form.html
@@ -0,0 +1,34 @@
+{% extends 'package/snippets/package_form.html' %}
+
+{% block stages %}
+ {% if form_style != 'edit' %}
+ {% if not h.ed_is_admin(c.user) %}
+
+
+ {% trans %}Note: This dataset will be submited for an administrator approval!{% endtrans %}
+
+
+ {% endif %}
+ {{ super() }}
+ {% endif %}
+{% endblock %}
+
+{% block save_button_text %}
+ {% if form_style != 'edit' %}
+ {{ super() }}
+ {% else %}
+ {{ _('Update Dataset') }}
+ {% endif %}
+{% endblock %}
+
+{% block cancel_button %}
+ {% if form_style != 'edit' %}
+ {{ super() }}
+ {% endif %}
+{% endblock %}
+
+{% block delete_button %}
+ {% if form_style == 'edit' and h.check_access('package_delete', {'id': pkg_dict.id}) %}
+ {{ super() }}
+ {% endif %}
+{% endblock %}
diff --git a/ckanext/ed/templates/package/snippets/stages.html b/ckanext/ed/templates/package/snippets/stages.html
new file mode 100644
index 0000000..8092be5
--- /dev/null
+++ b/ckanext/ed/templates/package/snippets/stages.html
@@ -0,0 +1,48 @@
+{#
+Inserts a stepped progress indicator for the new package form. Each stage can
+have one of three states, "uncomplete", "complete" and "active".
+
+stages - A list of states for each of the three stages. Missing stages default
+ to "uncomplete".
+
+Example:
+
+ {% snippet 'package/snippets/stages.html', stages=['active'] %}
+ {% snippet 'package/snippets/stages.html', stages=['complete', 'active'] %}
+ {% snippet 'package/snippets/stages.html', stages=['active', 'complete'] %}
+
+#}
+{% set s1 = stages[0] or 'active' %}
+{% set s2 = stages[1] or 'uncomplete' %}
+{% if s1 != 'uncomplete' %}{% set class = 'stage-1' %}{% endif %}
+{% if s2 != 'uncomplete' %}{% set class = 'stage-2' %}{% endif %}
+
+
+ -
+ {% if s1 != 'complete' %}
+ {% if h.ed_is_admin(c.user) %}
+ {{ _('Create dataset') }}
+ {% else %}
+ {{ _('Request dataset') }}
+ {% endif %}
+ {% else %}
+ {% if h.ed_is_admin(c.user) %}
+
+ {% else %}
+
+ {% endif %}
+ {% endif %}
+
+ -
+ {% if s2 != 'complete' %}
+ {{ _('Add data') }}
+ {% else %}
+ {% if s1 == 'active' %}
+ {# stage 1 #}
+
+ {% else %}
+ {% link_for _('Add data'), named_route='dataset.new', class_="highlight" %}
+ {% endif %}
+ {% endif %}
+
+
diff --git a/ckanext/ed/tests/test_helpers.py b/ckanext/ed/tests/test_helpers.py
index 40cccd4..fb826d8 100644
--- a/ckanext/ed/tests/test_helpers.py
+++ b/ckanext/ed/tests/test_helpers.py
@@ -7,19 +7,37 @@
class TestHelpers(test_helpers.FunctionalTestBase):
def test_get_recently_updated_datasets(self):
- factories.Dataset()
- factories.Dataset()
- factories.Dataset()
- dataset = factories.Dataset()
+ user = core_factories.User()
+ org = core_factories.Organization(
+ users=[{'name': user['name'], 'capacity': 'admin'}]
+ )
+ factories.Dataset(owner_org=org['id'])
+ factories.Dataset(owner_org=org['id'])
+ factories.Dataset(owner_org=org['id'])
+ dataset = factories.Dataset(owner_org=org['id'])
result = helpers.get_recently_updated_datasets()
- assert len(result) == 4
+ assert len(result) == 4, 'Epextec 4 but got %s' % len(result)
assert result[0]['id'] == dataset['id']
result = helpers.get_recently_updated_datasets(limit=2)
assert len(result) == 2
assert result[0]['id'] == dataset['id']
+ def test_get_recently_updated_datasets_lists_only_approved(self):
+ user = core_factories.User()
+ org = core_factories.Organization(
+ users=[{'name': user['name'], 'capacity': 'admin'}]
+ )
+ factories.Dataset(owner_org=org['id'], approval_state='approval_pending')
+ factories.Dataset(owner_org=org['id'], approval_state='approval_pending')
+ factories.Dataset(owner_org=org['id'])
+ dataset = factories.Dataset(owner_org=org['id'])
+
+ result = helpers.get_recently_updated_datasets()
+ assert len(result) == 2, 'Epextec 2 but got %s' % len(result)
+ assert result[0]['id'] == dataset['id']
+
def test_get_groups(self):
group1 = core_factories.Group()
@@ -31,3 +49,24 @@ def test_get_groups(self):
result = helpers.get_groups()
assert result[0]['id'] == group1['id']
assert len(result) == 4
+
+ def test_is_admin(self):
+ core_factories.User(name='george')
+ core_factories.User(name='john')
+ core_factories.User(name='paul')
+ core_factories.Organization(
+ users=[
+ {'name': 'george', 'capacity': 'admin'},
+ {'name': 'john', 'capacity': 'editor'},
+ {'name': 'paul', 'capacity': 'reader'}
+ ]
+ )
+
+ result = helpers.is_admin('george')
+ assert result, '%s is not True' % result
+ result = helpers.is_admin('john')
+ assert not result, '%s is not False' % result
+ result = helpers.is_admin('paul')
+ assert not result, '%s is not False' % result
+ result = helpers.is_admin('ringo')
+ assert not result, '%s is not False' % result
diff --git a/ckanext/ed/tests/test_validators.py b/ckanext/ed/tests/test_validators.py
new file mode 100644
index 0000000..70efc7e
--- /dev/null
+++ b/ckanext/ed/tests/test_validators.py
@@ -0,0 +1,62 @@
+from nose.tools import assert_equals
+
+from ckan import model
+from ckan.tests import factories as core_factories
+from ckan.tests.helpers import call_action, FunctionalTestBase
+
+
+class TestValidators(FunctionalTestBase):
+ def test_dataset_by_sysadmin_and_admin_is_not_approval_pending(self):
+ core_factories.User(name='george')
+ core_factories.Organization(
+ users=[{'name': 'george', 'capacity': 'admin'}],
+ name='us-ed-1',
+ id='us-ed-1'
+ )
+
+ sysadmin = core_factories.Sysadmin()
+ context = _create_context(sysadmin)
+ data_dict = _create_dataset_dict('test-dataset-1', 'us-ed-1')
+ call_action('package_create', context, **data_dict)
+ dataset = call_action('package_show', context, id='test-dataset-1')
+ assert_equals(dataset.get('approval_state'), 'active')
+
+ context = _create_context({'name': 'george'})
+ data_dict = _create_dataset_dict('test-dataset-2', 'us-ed-1')
+ call_action('package_create', context, **data_dict)
+ dataset = call_action('package_show', context, id='test-dataset-2')
+ assert_equals(dataset.get('approval_state'), 'active')
+
+
+ def test_dataset_by_editor_is_approval_pending(self):
+ core_factories.User(name='john')
+ core_factories.Organization(
+ users=[{'name': 'john', 'capacity': 'editor'}],
+ name='us-ed-2',
+ id='us-ed-2'
+ )
+
+ context = _create_context({'name': 'john'})
+ data_dict = _create_dataset_dict('test-dataset', 'us-ed-2')
+ call_action('package_create', context, **data_dict)
+ dataset = call_action('package_show', context, id='test-dataset')
+ assert_equals(dataset['approval_state'], 'approval_pending')
+
+
+def _create_context(user):
+ return {'model': model, 'user': user['name']}
+
+
+def _create_dataset_dict(package_name, office_name='us-ed'):
+ return {
+ 'name': package_name,
+ 'contact_name': 'Stu Shepard',
+ 'program_code': '321',
+ 'access_level': 'public',
+ 'bureau_code': '123',
+ 'contact_email': '%s@email.com' % package_name,
+ 'notes': 'notes',
+ 'owner_org': office_name,
+ 'title': 'Title',
+ 'identifier': 'identifier'
+ }
diff --git a/ckanext/ed/tests/test_workflow.py b/ckanext/ed/tests/test_workflow.py
new file mode 100644
index 0000000..d187d01
--- /dev/null
+++ b/ckanext/ed/tests/test_workflow.py
@@ -0,0 +1,112 @@
+from nose.tools import assert_raises, assert_equals
+
+from ckan.lib.search import rebuild
+from ckan.tests import factories as core_factories
+from ckan.tests import helpers as test_helpers
+from ckan.tests.helpers import call_action, FunctionalTestBase
+import ckan.plugins.toolkit as toolkit
+
+from ckanext.ed.tests import factories
+
+
+class TestWorFlows(FunctionalTestBase):
+ # For some reasons @classmethod does not work
+ def setup(self):
+ self.pkg_1 = 'test-dataset-1'
+ self.pkg_2 = 'test-dataset-2'
+ test_helpers.reset_db()
+ rebuild()
+ core_factories.User(name='george')
+ core_factories.User(name='john')
+ core_factories.User(name='paul')
+ core_factories.Organization(
+ users=[
+ {'name': 'george', 'capacity': 'admin'},
+ {'name': 'john', 'capacity': 'editor'},
+ {'name': 'paul', 'capacity': 'reader'}
+ ],
+ name='us-ed-1',
+ id='us-ed-1'
+ )
+ # Dataset created by factories seem to use sysadmin so approval_state
+ # forced to be "approved". Creating packages this way to avoid that
+ context = {'user': 'john'}
+ data_dict = _create_dataset_dict(self.pkg_1, 'us-ed-1')
+ call_action('package_create', context, **data_dict)
+
+ # 1 datasets above "approveal_pending" and 1 below "approved"
+ context = {'user': 'george'}
+ data_dict = _create_dataset_dict(self.pkg_2, 'us-ed-1')
+ call_action('package_create', context, **data_dict)
+
+
+ @classmethod
+ def tearDownClass(self):
+ pass
+
+
+ def test_dataset_not_appears_in_search_if_not_approved_admin(self):
+ context = {'user': 'george'}
+ packages = call_action('package_search', context, **{})
+ assert_equals(packages['count'], 1)
+
+ def test_dataset_not_appears_in_search_if_not_approved_editor(self):
+ context = {'user': 'john'}
+ packages = call_action('package_search', context, **{})
+ assert_equals(packages['count'], 1)
+
+ def test_dataset_not_appears_in_search_if_not_approved_reder(self):
+ context = {'user': 'paul'}
+ packages = call_action('package_search', context, **{})
+ assert_equals(packages['count'], 1)
+
+ def test_dataset_not_appears_in_search_if_not_approved_aninimous(self):
+ context = {'user': 'ringo'}
+ packages = call_action('package_search', context, **{})
+ assert_equals(packages['count'], 1)
+
+ def test_package_show_admin_can_see_both(self):
+ context = {'user': 'george'}
+ packages = call_action('package_show', context, **{'id': self.pkg_1})
+ assert_equals(packages['name'], self.pkg_1)
+ packages = call_action('package_show', context, **{'id': self.pkg_2})
+ assert_equals(packages['name'], self.pkg_2)
+
+ def test_package_show_editor_can_see_both(self):
+ context = {'user': 'john'}
+ packages = call_action('package_show', context, **{'id': self.pkg_1})
+ assert_equals(packages['name'], self.pkg_1)
+ packages = call_action('package_show', context, **{'id': self.pkg_2})
+ assert_equals(packages['name'], self.pkg_2)
+
+ def test_package_show_member_can_not_see_pending(self):
+ context = {'user': 'paul', 'ignore_auth': False}
+ assert_raises(toolkit.ObjectNotFound,
+ call_action, 'package_show', context, id=self.pkg_1)
+ packages = call_action('package_show', context, **{'id': self.pkg_2})
+ assert_equals(packages['name'], self.pkg_2)
+
+ def test_package_show_anonimous_can_not_see_pending(self):
+ context = {'user': 'ringo', 'ignore_auth': False}
+ assert_raises(toolkit.ObjectNotFound,
+ call_action, 'package_show', context, id=self.pkg_1)
+ packages = call_action('package_show', context, **{'id': self.pkg_2})
+ assert_equals(packages['name'], self.pkg_2)
+
+
+# Helpers
+
+
+def _create_dataset_dict(package_name, office_name='us-ed'):
+ return {
+ 'name': package_name,
+ 'contact_name': 'Stu Shepard',
+ 'program_code': '321',
+ 'access_level': 'public',
+ 'bureau_code': '123',
+ 'contact_email': '%s@email.com' % package_name,
+ 'notes': 'notes',
+ 'owner_org': office_name,
+ 'title': 'Title',
+ 'identifier': 'identifier'
+ }
diff --git a/ckanext/ed/validators.py b/ckanext/ed/validators.py
new file mode 100644
index 0000000..40fd034
--- /dev/null
+++ b/ckanext/ed/validators.py
@@ -0,0 +1,22 @@
+from ckan.plugins import toolkit
+
+
+def state_validator(key, data, errors, context):
+ user_orgs = toolkit.get_action('organization_list_for_user')(
+ context, {'id': context['user']})
+ office_id = data.get(('owner_org',))
+
+ state = data.pop(key, None)
+
+ # If the user is member of the organization but not an admin, force the
+ # state to be pending
+ for org in user_orgs:
+ if org.get('id') == office_id:
+ if org.get('capacity') == 'admin':
+ # If no state provided and user is an admin, default to active
+ state = state or 'active'
+ else:
+ # If not admin, create as pending
+ state = 'approval_pending'
+
+ data[key] = state