Skip to content

Commit

Permalink
global: Community admin and member roles added
Browse files Browse the repository at this point in the history
* NEW Adds Community admin and member roles.

* NEW Creates publication workflows and adds "direct publish"
  workflow.

* NEW Enables communities to restrict new submissions to their
  members.

Signed-off-by: Nicolas Harraudeau <nicolas.harraudeau@cern.ch>
  • Loading branch information
Nicolas Harraudeau committed Nov 8, 2016
1 parent c96075c commit d577541
Show file tree
Hide file tree
Showing 16 changed files with 450 additions and 90 deletions.
4 changes: 3 additions & 1 deletion b2share/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@

# FIXME disable authentication by default as B2Access integration is not yet
# done.
B2SHARE_COMMUNITIES_REST_ACCESS_CONTROL_DISABLED = True
B2SHARE_COMMUNITIES_REST_ACCESS_CONTROL_DISABLED = False

# Records
# =======
Expand Down Expand Up @@ -82,6 +82,7 @@
'application/json-patch+json':
lambda: request.get_json(force=True),
'application/json':
# FIXME: create a loader so that only allowed fields can be set
lambda: request.get_json(),
# 'b2share.modules.deposit.loaders:deposit_record_loader'
},
Expand Down Expand Up @@ -121,6 +122,7 @@
'application/json-patch+json':
lambda: request.get_json(force=True),
'application/json':
# FIXME: create a loader so that only allowed fields can be set
lambda: request.get_json(),
# 'b2share.modules.deposit.loaders:deposit_record_loader'
},
Expand Down
49 changes: 40 additions & 9 deletions b2share/modules/communities/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@
from invenio_db import db
from jsonpatch import apply_patch
from sqlalchemy.orm.exc import NoResultFound
from invenio_accounts.models import Role

from .errors import CommunityDeletedError, CommunityDoesNotExistError, \
InvalidCommunityError
from .signals import after_community_delete, after_community_insert, \
after_community_update, before_community_delete, before_community_insert, \
before_community_update
from .models import Community as CommunityMetadata, _communiy_admin_role_name, \
_communiy_member_role_name


class Community(object):
Expand Down Expand Up @@ -70,7 +73,6 @@ def get(cls, id=None, name=None, with_deleted=False):
ValueError: :attr:`id` and :attr:`name` are not set or both are
set.
"""
from .models import Community as CommunityMetadata
if not id and not name:
raise ValueError('"id" or "name" should be set.')
if id and name:
Expand All @@ -91,7 +93,6 @@ def get(cls, id=None, name=None, with_deleted=False):
@classmethod
def get_all(cls, start=None, stop=None, name=None):
"""Searches for matching communities."""
from .models import Community as CommunityMeta
if (start is None and stop is None):
if name is None:
metadata = CommunityMeta.query.order_by(CommunityMeta.created)
Expand All @@ -113,7 +114,9 @@ def get_all(cls, start=None, stop=None, name=None):
return [cls(md) for md in metadata]

@classmethod
def create_community(cls, name, description, logo=None, id_=None):
def create_community(cls, name, description, logo=None, id_=None,
publication_workflow=None,
restricted_submission=False):
"""Create a new Community.
A new community is implicitly associated with a new, empty, schema
Expand All @@ -131,7 +134,6 @@ def create_community(cls, name, description, logo=None, id_=None):
b2share.modules.communities.errors.InvalidCommunityError: The
community creation failed because the arguments are not valid.
"""
from .models import Community as CommunityMetadata
try:
with db.session.begin_nested():
kwargs = {}
Expand All @@ -155,7 +157,8 @@ def update(self, data, clear_fields=False):
Args:
data (dict): can have one of those fields: name, description, logo.
it replaces the given values.
clear_fields (bool): if True, set not specified fields to None.
clear_fields (bool): if True, set not specified fields to their
default value.
Returns:
:class:`Community`: self
Expand All @@ -171,9 +174,14 @@ def update(self, data, clear_fields=False):
if clear_fields:
for field in ['name', 'description', 'logo']:
setattr(self.model, field, data.get(field, None))
else:
for key, value in data.items():
setattr(self.model, key, value)
self.model.publication_workflow = \
'review_and_publish'
self.model.restricted_submission = False
# FIXME: what do we do when the publication_workflow is changed?
# Do we run the new workflow on all records in order to fix the
# their publication_state?
for key, value in data.items():
setattr(self.model, key, value)
db.session.merge(self.model)
except sqlalchemy.exc.IntegrityError as e:
raise InvalidCommunityError() from e
Expand Down Expand Up @@ -203,7 +211,8 @@ def patch(self, patch):
data = apply_patch({
'name': self.model.name,
'description': self.model.description,
'logo': self.model.logo
'logo': self.model.logo,
'publication_workflow': self.model.publication_workflow,
}, patch, True)
self.update(data)
return self
Expand Down Expand Up @@ -262,3 +271,25 @@ def description(self):
def logo(self):
"""Retrieve community's logo."""
return self.model.logo

@property
def publication_workflow(self):
"""Retrieve the name of the publication workflow."""
return self.model.publication_workflow

@property
def restricted_submission(self):
"""Retrieve the deposit creation restriction flag."""
return self.model.restricted_submission

@property
def admin_role(self):
"""Role given to this community's administrators."""
return Role.query.filter(
Role.name == _communiy_admin_role_name(self)).one()

@property
def member_role(self):
"""Role given to this community's members."""
return Role.query.filter(
Role.name == _communiy_member_role_name(self)).one()
8 changes: 8 additions & 0 deletions b2share/modules/communities/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

from __future__ import absolute_import

from invenio_rest.errors import RESTException


class InvalidCommunityError(Exception):
"""Exception raised when a community is invalid."""
Expand All @@ -40,3 +42,9 @@ class CommunityDeletedError(Exception):
"""Exception raised when a requested community is marked as deleted."""
pass


class InvalidPublicationStateError(RESTException):
"""Exception raised when a deposit is an invalid publication state."""

code = 400
"""HTTP Status code."""
2 changes: 0 additions & 2 deletions b2share/modules/communities/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from werkzeug.utils import cached_property

from . import config
from .views import blueprint

from .cli import communities as communities_cmd

Expand Down Expand Up @@ -68,7 +67,6 @@ def __init__(self, app=None):
def init_app(self, app):
"""Flask application initialization."""
self.init_config(app)
app.register_blueprint(blueprint)
app.cli.add_command(communities_cmd)
app.extensions['b2share-communities'] = _B2ShareCommunitiesState(app)

Expand Down
62 changes: 62 additions & 0 deletions b2share/modules/communities/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@
"""Community models."""

import uuid
from itertools import chain

from invenio_db import db
from sqlalchemy.sql import expression
from sqlalchemy_utils.models import Timestamp
from sqlalchemy_utils.types import UUIDType
from sqlalchemy import event
from invenio_accounts.models import Role
from invenio_access.models import ActionRoles


class Community(db.Model, Timestamp):
Expand Down Expand Up @@ -62,7 +66,65 @@ class Community(db.Model, Timestamp):
deleted = db.Column(db.Boolean, nullable=False,
server_default=expression.false())

# Publication workflow used in this community
publication_workflow = db.Column(db.String(80), nullable=False,
default='review_and_publish')

# Restrict record creation
restricted_submission = db.Column(db.Boolean, nullable=False,
server_default=expression.false(),
default=False)


def _communiy_admin_role_name(community):
"""Generate the name of the given community's admin role."""
return 'com:{0}:{1}'.format(community.id.hex, 'admin')


def _communiy_member_role_name(community):
"""Generate the name of the given community's member role."""
return 'com:{0}:{1}'.format(community.id.hex, 'member')


@event.listens_for(Community, 'after_insert')
def receive_before_insert(mapper, connection, target):
"""Create community admin and member roles and add their permissions."""
from b2share.modules.deposit.permissions import (
create_deposit_need_factory, read_deposit_need_factory,
)
from b2share.modules.deposit.api import PublicationStates

admin_role = Role(
name=_communiy_admin_role_name(target),
description='Admin role of the community "{}"'.format(target.name)
)
member_role = Role(
name=_communiy_member_role_name(target),
description='Member role of the community "{}"'.format(target.name)
)

db.session.add(admin_role)
db.session.add(member_role)
member_needs = [
create_deposit_need_factory(str(target.id)),
]
admin_needs = [
read_deposit_need_factory(
community=str(target.id),
publication_state=PublicationStates.submitted.name
),
read_deposit_need_factory(
community=str(target.id),
publication_state=PublicationStates.published.name
),
]
for need in member_needs:
db.session.add(ActionRoles.allow(need, role=member_role))
for need in chain (member_needs, admin_needs):
db.session.add(ActionRoles.allow(need, role=admin_role))


__all__ = (
'Community',
'CommunityRole',
)
2 changes: 2 additions & 0 deletions b2share/modules/communities/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def community_to_dict(community):
logo=community.logo,
created=community.created,
updated=community.updated,
publication_workflow=community.publication_workflow,
restricted_submission=community.restricted_submission,
links=dict(
self=community_self_link(community, _external=True)
)
Expand Down
79 changes: 79 additions & 0 deletions b2share/modules/communities/workflows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
#
# This file is part of EUDAT B2Share.
# Copyright (C) 2015, 2016, University of Tuebingen, CERN.
#
# B2Share is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# B2Share is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with B2Share; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
#
# In applying this license, CERN does not
# waive the privileges and immunities granted to it by virtue of its status
# as an Intergovernmental Organization or submit itself to any jurisdiction.

"""Publication workflows."""

from .errors import InvalidPublicationStateError

def review_and_publish_workflow(previous_model, new_deposit):
"""Workflow publishing the deposits on submission."""
# import ipdb
# ipdb.set_trace()
from b2share.modules.deposit.api import PublicationStates
new_state = new_deposit['publication_state']
previous_state = previous_model.json['publication_state']
if previous_state != new_state:
transition = (previous_state, new_state)
# Check that the transition is a valid one
if transition not in [
(PublicationStates.draft.name, PublicationStates.submitted.name),
(PublicationStates.submitted.name, PublicationStates.draft.name),
(PublicationStates.submitted.name,
PublicationStates.published.name),
]:
raise InvalidPublicationStateError(
description='Transition from publication state {0} to {1} is'
'not allowed by community\'s workflow {2}'.format(
previous_state, new_state, 'review_and_publish'
)
)


def direct_publish_workflow(previous_model, new_deposit):
"""Workflow publishing the deposits on submission."""
from b2share.modules.deposit.api import PublicationStates

new_state = new_deposit['publication_state']
previous_state = previous_model.json['publication_state']
if previous_state != new_state:
transition = (previous_state, new_state)
# Check that the transition is a valid one
if transition not in [
(PublicationStates.draft.name, PublicationStates.submitted.name),
(PublicationStates.draft.name, PublicationStates.published.name),
]:
raise InvalidPublicationStateError(
description='Transition from publication state {0} to {1} is '
'not allowed by community\'s workflow {2}'.format(
previous_state, new_state, 'review_and_publish'
)
)
# Publish automatically when submitted
if new_state == PublicationStates.submitted.name:
new_deposit['publication_state'] = PublicationStates.published.name


publication_workflows = {
'review_and_publish': review_and_publish_workflow,
'direct_publish': direct_publish_workflow,
}
13 changes: 5 additions & 8 deletions b2share/modules/deposit/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
from invenio_records_files.api import Record

from .errors import InvalidDepositDataError, InvalidDepositStateError
from b2share.modules.communities.api import Community
from b2share.modules.communities.workflows import publication_workflows
from invenio_records.errors import MissingModelError
from b2share.modules.records.errors import InvalidRecordError
from b2share.modules.access.policies import is_under_embargo
Expand Down Expand Up @@ -146,14 +148,9 @@ def commit(self):
if is_under_embargo(self):
self['open_access'] = False

# test invalid state transitions
if (self['publication_state'] == PublicationStates.submitted.name
and self.model.json['publication_state'] !=
PublicationStates.draft.name):
raise InvalidDepositStateError(
'Cannot submit a deposit in {} state'.format(
self.model.json['publication_state'])
)
community = Community.get(self['community'])
workflow = publication_workflows[community.publication_workflow]
workflow(self.model, self)

# publish the deposition if needed
if (self['publication_state'] == PublicationStates.published.name
Expand Down
Loading

0 comments on commit d577541

Please sign in to comment.