Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1038 organization hierarchy #1247

Merged
merged 42 commits into from
Dec 10, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
35fc3ed
[xs] Helpful SOLR hint.
May 25, 2012
f8dbc69
Merge branch 'master' of ssh://github.com/okfn/ckan
Jun 15, 2012
04ca419
Fix minor bug that caused create_users to not commit changes.
Jun 15, 2012
6780e91
Improve logging in useful places.
Jun 15, 2012
75ba310
Comment required, else Toby would just delete the method out of hand.
Jun 21, 2012
98888d2
[#1038] Correct copyright sign for UTF8 file and cut/paste error.
Jul 22, 2013
21cd0ac
[#1038] Fix to allow sqlite testing - for until the tests are overhau…
Jul 22, 2013
96f0a3e
[#1038] Test fixtures for organization hierarchy.
Jul 22, 2013
fde50fa
[#1038] Model methods for organization hierarchy.
Jul 22, 2013
2c0bbaf
[#1038] Test data is now organizations rather than groups.
Jul 23, 2013
1229ff2
[#1038] Docs for Member and its capacities is most handy. Explained t…
Jul 23, 2013
854015a
Merge branch 'master' of github.com:datagovuk/ckan
Jul 23, 2013
a7a2714
Merge branch 'master' of github.com:okfn/ckan
Jul 23, 2013
c098309
Merge branch 'master' into 1038-organization-hierarchy
Jul 23, 2013
68bfd7a
[#1038] Permission cascading code with tests.
Jul 30, 2013
4db2d24
[#1038] Fix permission checking for organizations. Corrected bad test.
Jul 30, 2013
6d97199
[#1038] Fix unreliable ordering of upward CTE.
Jul 30, 2013
66c9dff
Merge branch 'master' of github.com:okfn/ckan into 1038-organization-…
Aug 23, 2013
7ce1555
[#1038] Fix up authz config error and deleted groups
Sep 6, 2013
f7f5049
[#1038] PEP8 fixes only.
Sep 6, 2013
af6c039
[#1038] Reverse the meaning of group member of group.
Sep 12, 2013
34b3fb7
[noticket] Fix paster commands that fail due to
Sep 12, 2013
527f3e9
[#1038] Fix display of tree hierarchy, broken in reversal af6c039.
Sep 12, 2013
8babc4f
[#1038] Setting of parent group in the form is now done via group['gr…
Sep 17, 2013
407cf91
[#1038] Fix group deletion mid-hierarchy.
Sep 17, 2013
df890fc
[#1038] Config option fixed and documented.
Sep 17, 2013
e023b44
[#1038] Fix an apparently botched merge.
Sep 17, 2013
4290821
[#1038] Fix an apparently botched merge (cont).
Sep 17, 2013
24548c0
[#1038] Fix an apparently botched merge (cont).
Sep 17, 2013
e0bc490
Merge branch 'master' of github.com:okfn/ckan into 1038-organization-…
Sep 17, 2013
fafd9f9
[#1038] Fix reading config.
Sep 17, 2013
b7136b8
[#1038] Fix top level groups including deleted ones.
Sep 23, 2013
1be9681
[noticket] Fix displaying About page for organizations.
Sep 24, 2013
ed78fc9
[#1038] Improve performance of viewing hierarchy, avoiding groups as …
Sep 24, 2013
218f73b
[#1038] get_parent_groups added
Sep 24, 2013
18d3c8b
[noticket] Fix organization template that checked package_create auth…
Sep 30, 2013
399a6a4
[#1038] Add a hard limit to recursion - just in case.
Oct 3, 2013
b95ef59
[#1038] merge confilcts
kindly Nov 4, 2013
22a3432
[#1038] Fix for python 2.6.
Dec 9, 2013
75394f8
[#1038] Simplify code that reads ckan.auth config.
Dec 9, 2013
8dffa31
Merge branch 'master' of github.com:okfn/ckan into 1038-organization-…
Dec 9, 2013
1de51c6
[#1038] Fix a bug introduced on this branch, for pre-filling the grou…
Dec 9, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ckan/config/deployment.ini_tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ ckan.auth.user_delete_groups = true
ckan.auth.user_delete_organizations = true
ckan.auth.create_user_via_api = false
ckan.auth.create_user_via_web = true
ckan.auth.roles_that_cascade_to_sub_groups = admin


## Search Settings
Expand Down
6 changes: 4 additions & 2 deletions ckan/config/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,10 @@ def template_loaded(template):
# Here we create the site user if they are not already in the database
try:
logic.get_action('get_site_user')({'ignore_auth': True}, None)
except sqlalchemy.exc.ProgrammingError:
# The database is not initialised. This is a bit dirty.
except (sqlalchemy.exc.ProgrammingError, sqlalchemy.exc.OperationalError):
# (ProgrammingError for Postgres, OperationalError for SQLite)
# The database is not initialised. This is a bit dirty. This occurs
# when running tests.
pass
except sqlalchemy.exc.InternalError:
# The database is not initialised. Travis hits this
Expand Down
5 changes: 4 additions & 1 deletion ckan/lib/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1322,7 +1322,7 @@ class CreateTestDataCommand(CkanCommand):
translations of terms
create-test-data vocabs - annakerenina, warandpeace, and some test
vocabularies

create-test-data hierarchy - hierarchy of groups
'''
summary = __doc__.split('\n')[0]
usage = __doc__
Expand All @@ -1332,6 +1332,7 @@ class CreateTestDataCommand(CkanCommand):
def command(self):
self._load_config()
self._setup_app()
from ckan import plugins
from create_test_data import CreateTestData

if self.args:
Expand All @@ -1356,6 +1357,8 @@ def command(self):
CreateTestData.create_translations_test_data()
elif cmd == 'vocabs':
CreateTestData.create_vocabs_test_data()
elif cmd == 'hierarchy':
CreateTestData.create_group_hierarchy_test_data()
else:
print 'Command %s not recognized' % cmd
raise NotImplementedError
Expand Down
150 changes: 128 additions & 22 deletions ckan/lib/create_test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ def create_family_test_data(cls, extra_users=[]):
relationships=family_relationships,
extra_user_names=extra_users)

@classmethod
def create_group_hierarchy_test_data(cls, extra_users=[]):
cls.create_users(group_hierarchy_users)
cls.create_groups(group_hierarchy_groups)
cls.create_arbitrary(group_hierarchy_datasets)

@classmethod
def create_test_user(cls):
tester = model.User.by_name(u'tester')
Expand Down Expand Up @@ -215,18 +221,26 @@ def create_arbitrary(cls, package_dicts, relationships=[],
group = model.Group.by_name(unicode(group_name))
if not group:
if not group_name in new_groups:
group = model.Group(name=unicode(group_name))
group = model.Group(name=
unicode(group_name))
model.Session.add(group)
new_group_names.add(group_name)
new_groups[group_name] = group
else:
# If adding multiple packages with the same group name,
# model.Group.by_name will not find the group as the
# session has not yet been committed at this point.
# Fetch from the new_groups dict instead.
# If adding multiple packages with the same
# group name, model.Group.by_name will not
# find the group as the session has not yet
# been committed at this point. Fetch from
# the new_groups dict instead.
group = new_groups[group_name]
member = model.Member(group=group, table_id=pkg.id, table_name='package')
capacity = 'organization' if group.is_organization\
else 'public'
member = model.Member(group=group, table_id=pkg.id,
table_name='package',
capacity=capacity)
model.Session.add(member)
if group.is_organization:
pkg.owner_org = group.id
elif attr == 'license':
pkg.license_id = val
elif attr == 'license_id':
Expand Down Expand Up @@ -315,38 +329,67 @@ def pkg(pkg_name):
@classmethod
def create_groups(cls, group_dicts, admin_user_name=None, auth_profile=""):
'''A more featured interface for creating groups.
All group fields can be filled, packages added and they can
have an admin user.'''
All group fields can be filled, packages added, can have
an admin user and be a member of other groups.'''
rev = model.repo.new_revision()
# same name as user we create below
rev.author = cls.author
if admin_user_name:
admin_users = [model.User.by_name(admin_user_name)]
else:
admin_users = []
assert isinstance(group_dicts, (list, tuple))
group_attributes = set(('name', 'title', 'description', 'parent_id'))
group_attributes = set(('name', 'title', 'description', 'parent_id',
'type', 'is_organization'))
for group_dict in group_dicts:
if model.Group.by_name(group_dict['name']):
log.warning('Cannot create group "%s" as it already exists.' % \
(group_dict['name']))
if model.Group.by_name(unicode(group_dict['name'])):
log.warning('Cannot create group "%s" as it already exists.' %
group_dict['name'])
continue
pkg_names = group_dict.pop('packages', [])
group = model.Group(name=unicode(group_dict['name']))
group.type = auth_profile or 'group'
for key in group_dict:
if key in group_attributes:
setattr(group, key, group_dict[key])
else:
elif key not in ('admins', 'editors', 'parent'):
group.extras[key] = group_dict[key]
assert isinstance(pkg_names, (list, tuple))
for pkg_name in pkg_names:
pkg = model.Package.by_name(unicode(pkg_name))
assert pkg, pkg_name
member = model.Member(group=group, table_id=pkg.id, table_name='package')
member = model.Member(group=group, table_id=pkg.id,
table_name='package')
model.Session.add(member)
model.Session.add(group)
model.setup_default_user_roles(group, admin_users)
admins = [model.User.by_name(user_name)
for user_name in group_dict.get('admins', [])] + \
admin_users
for admin in admins:
member = model.Member(group=group, table_id=admin.id,
table_name='user', capacity='admin')
model.Session.add(member)
editors = [model.User.by_name(user_name)
for user_name in group_dict.get('editors', [])]
for editor in editors:
member = model.Member(group=group, table_id=editor.id,
table_name='user', capacity='editor')
model.Session.add(member)
# Need to commit the current Group for two reasons:
# 1. It might have a parent, and the Member will need the Group.id
# value allocated on commit.
# 2. The next Group created may have this Group as a parent so
# creation of the Member needs to refer to this one.
model.Session.commit()
rev = model.repo.new_revision()
rev.author = cls.author
# add it to a parent's group
if 'parent' in group_dict:
parent = model.Group.by_name(unicode(group_dict['parent']))
assert parent, group_dict['parent']
member = model.Member(group=group, table_id=parent.id,
table_name='group', capacity='parent')
model.Session.add(member)
#model.setup_default_user_roles(group, admin_users)
cls.group_names.add(group_dict['name'])
model.repo.commit_and_remove()

Expand All @@ -362,7 +405,8 @@ def create(cls, auth_profile="", package_type=None):
* Associated tags, etc etc
'''
if auth_profile == "publisher":
organization_group = model.Group(name=u"organization_group", type="organization")
organization_group = model.Group(name=u"organization_group",
type="organization")

cls.pkg_names = [u'annakarenina', u'warandpeace']
pkg1 = model.Package(name=cls.pkg_names[0], type=package_type)
Expand Down Expand Up @@ -483,11 +527,12 @@ def create(cls, auth_profile="", package_type=None):
roger = model.Group.by_name(u'roger')
model.setup_default_user_roles(david, [russianfan])
model.setup_default_user_roles(roger, [russianfan])
model.add_user_to_role(visitor, model.Role.ADMIN, roger)

# in new_authz you can't give a visitor permissions to a
# group it seems, so this is a bit meaningless
model.add_user_to_role(visitor, model.Role.ADMIN, roger)
model.repo.commit_and_remove()


# method used in DGU and all good tests elsewhere
@classmethod
def create_users(cls, user_dicts):
Expand All @@ -501,9 +546,11 @@ def create_users(cls, user_dicts):

@classmethod
def _create_user_without_commit(cls, name='', **user_dict):
if model.User.by_name(name) or (user_dict.get('open_id') and model.User.by_openid(user_dict.get('openid'))):
log.warning('Cannot create user "%s" as it already exists.' % \
(name or user_dict['name']))
if model.User.by_name(name) or \
(user_dict.get('open_id') and
model.User.by_openid(user_dict.get('openid'))):
log.warning('Cannot create user "%s" as it already exists.' %
name or user_dict['name'])
return
# User objects are not revisioned so no need to create a revision
user_ref = name or user_dict['openid']
Expand Down Expand Up @@ -826,6 +873,65 @@ def make_some_vocab_tags(cls):
}
]

group_hierarchy_groups = [
{'name': 'department-of-health',
'title': 'Department of Health',
'contact-email': 'contact@doh.gov.uk',
'type': 'organization',
'is_organization': True
},
{'name': 'food-standards-agency',
'title': 'Food Standards Agency',
'contact-email': 'contact@fsa.gov.uk',
'parent': 'department-of-health',
'type': 'organization',
'is_organization': True},
{'name': 'national-health-service',
'title': 'National Health Service',
'contact-email': 'contact@nhs.gov.uk',
'parent': 'department-of-health',
'type': 'organization',
'is_organization': True,
'editors': ['nhseditor'],
'admins': ['nhsadmin']},
{'name': 'nhs-wirral-ccg',
'title': 'NHS Wirral CCG',
'contact-email': 'contact@wirral.nhs.gov.uk',
'parent': 'national-health-service',
'type': 'organization',
'is_organization': True,
'editors': ['wirraleditor'],
'admins': ['wirraladmin']},
{'name': 'nhs-southwark-ccg',
'title': 'NHS Southwark CCG',
'contact-email': 'contact@southwark.nhs.gov.uk',
'parent': 'national-health-service',
'type': 'organization',
'is_organization': True},
{'name': 'cabinet-office',
'title': 'Cabinet Office',
'contact-email': 'contact@cabinet-office.gov.uk',
'type': 'organization',
'is_organization': True},
]

group_hierarchy_datasets = [
{'name': 'doh-spend', 'title': 'Department of Health Spend Data',
'groups': ['department-of-health']},
{'name': 'nhs-spend', 'title': 'NHS Spend Data',
'groups': ['national-health-service']},
{'name': 'wirral-spend', 'title': 'Wirral Spend Data',
'groups': ['nhs-wirral-ccg']},
{'name': 'southwark-spend', 'title': 'Southwark Spend Data',
'groups': ['nhs-southwark-ccg']},
]

group_hierarchy_users = [{'name': 'nhsadmin', 'password': 'pass'},
{'name': 'nhseditor', 'password': 'pass'},
{'name': 'wirraladmin', 'password': 'pass'},
{'name': 'wirraleditor', 'password': 'pass'},
]

# Some test terms and translations.
terms = ('A Novel By Tolstoy',
'Index of the novel',
Expand Down
6 changes: 5 additions & 1 deletion ckan/lib/dictization/model_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,11 @@ def group_member_save(context, group_dict, member_table_name):
entities = {}
Member = model.Member

ModelClass = getattr(model, member_table_name[:-1].capitalize())
classname = member_table_name[:-1].capitalize()
if classname == 'Organization':
# Organizations use the model.Group class
classname = 'Group'
ModelClass = getattr(model, classname)

for entity_dict in entity_list:
name_or_id = entity_dict.get('id') or entity_dict.get('name')
Expand Down
4 changes: 2 additions & 2 deletions ckan/lib/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def register_group_plugins(map):
"""
Register the various IGroupForm instances.

This method will setup the mappings between package types and the
This method will setup the mappings between group types and the
registered IGroupForm instances. If it's called more than once an
exception will be raised.
"""
Expand Down Expand Up @@ -160,7 +160,7 @@ def register_group_plugins(map):

if group_type in _group_plugins:
raise ValueError, "An existing IGroupForm is "\
"already associated with the package type "\
"already associated with the group type "\
"'%s'" % group_type
_group_plugins[group_type] = plugin

Expand Down
9 changes: 0 additions & 9 deletions ckan/logic/action/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,6 @@ def _group_or_org_create(context, data_dict, is_org=False):
model = context['model']
user = context['user']
session = context['session']
parent = context.get('parent', None)
data_dict['is_organization'] = is_org

upload = uploader.Upload('group')
Expand Down Expand Up @@ -536,14 +535,6 @@ def _group_or_org_create(context, data_dict, is_org=False):

group = model_save.group_dict_save(data, context)

if parent:
parent_group = model.Group.get( parent )
if parent_group:
member = model.Member(group=parent_group, table_id=group.id, table_name='group')
session.add(member)
log.debug('Group %s is made child of group %s',
group.name, parent_group.name)

if user:
admins = [model.User.by_name(user.decode('utf8'))]
else:
Expand Down
11 changes: 11 additions & 0 deletions ckan/logic/action/delete.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'''API functions for deleting data from CKAN.'''

from sqlalchemy import or_

import ckan.logic
import ckan.logic.action
import ckan.plugins as plugins
Expand Down Expand Up @@ -275,6 +277,15 @@ def _group_or_org_delete(context, data_dict, is_org=False):
rev = model.repo.new_revision()
rev.author = user
rev.message = _(u'REST API: Delete %s') % revisioned_details

# The group's Member objects are deleted
# (including hierarchy connections to parent and children groups)
for member in model.Session.query(model.Member).\
filter(or_(model.Member.table_id == id,
model.Member.group_id == id)).\
filter(model.Member.state == 'active').all():
member.delete()

group.delete()

if is_org:
Expand Down
18 changes: 0 additions & 18 deletions ckan/logic/action/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,6 @@ def _group_or_org_update(context, data_dict, is_org=False):
user = context['user']
session = context['session']
id = _get_or_bust(data_dict, 'id')
parent = context.get('parent', None)

group = model.Group.get(id)
context["group"] = group
Expand Down Expand Up @@ -477,23 +476,6 @@ def _group_or_org_update(context, data_dict, is_org=False):
context['prevent_packages_update'] = True
group = model_save.group_dict_save(data, context)

if parent:
parent_group = model.Group.get( parent )
if parent_group and not parent_group in group.get_groups(group.type):
# Delete all of this groups memberships
current = session.query(model.Member).\
filter(model.Member.table_id == group.id).\
filter(model.Member.table_name == "group").all()
if current:
log.debug('Parents of group %s deleted: %r', group.name,
[membership.group.name for membership in current])
for c in current:
session.delete(c)
member = model.Member(group=parent_group, table_id=group.id, table_name='group')
session.add(member)
log.debug('Group %s is made child of group %s',
group.name, parent_group.name)

if is_org:
plugin_type = plugins.IOrganizationController
else:
Expand Down
Loading