From 3068d7d4c8353defacd0a212a574597751a79377 Mon Sep 17 00:00:00 2001 From: Anna Trzcinska Date: Mon, 4 May 2020 10:35:11 +0200 Subject: [PATCH] global: start registering activities Signed-off-by: Anna Trzcinska --- cap/activities.py | 71 ++++++++++++++++ ...3ecb_add_table_for_tracking_acitivities.py | 43 ++++++++++ cap/config.py | 2 + cap/modules/deposit/api.py | 83 +++++-------------- requirements-local-forks.txt | 1 + requirements.txt | 2 +- 6 files changed, 138 insertions(+), 64 deletions(-) create mode 100644 cap/activities.py create mode 100644 cap/alembic/bceec8433ecb_add_table_for_tracking_acitivities.py diff --git a/cap/activities.py b/cap/activities.py new file mode 100644 index 0000000000..b3420f4c0d --- /dev/null +++ b/cap/activities.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# +# This file is part of CERN Analysis Preservation Framework. +# Copyright (C) 2016 CERN. +# +# CERN Analysis Preservation Framework 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. +# +# CERN Analysis Preservation Framework 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 CERN Analysis Preservation Framework; 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. +"""CAP utilities for registering activities with SQL-Continuum plugin.""" +from flask import current_app +from invenio_db import db + +# list of available activities -> message to show to user (for serializers) +ACTIVITIES = { + 'create deposit': None, + 'update permissions': None, + 'publish analysis': None, + 'reedit published analysis': None, + 'update analysis': None, + 'patch analysis': None, +} + + +def get_activity_model(): + """Return SQLAlchemy activities model if registered.""" + return getattr(current_app.extensions['invenio-db'].versioning_manager, + 'activity_cls', None) + + +def register_activity(verb, + object=None, + object_id=None, + data=None, + target=None, + target_id=None): + """Register activity if activities plugin on.""" + + activity_model = get_activity_model() + + # check if activities plugin is there + if not activity_model: + return + + assert isinstance(object_id, int) if object_id else True + assert isinstance(target_id, int) if target_id else True + + assert verb in ACTIVITIES.keys() + + with db.session.begin_nested(): + db.session.add( + activity_model(verb=verb, + data=data, + object=object, + object_id=object_id, + target=target, + target_id=target_id)) diff --git a/cap/alembic/bceec8433ecb_add_table_for_tracking_acitivities.py b/cap/alembic/bceec8433ecb_add_table_for_tracking_acitivities.py new file mode 100644 index 0000000000..22db95e211 --- /dev/null +++ b/cap/alembic/bceec8433ecb_add_table_for_tracking_acitivities.py @@ -0,0 +1,43 @@ +# +# This file is part of Invenio. +# Copyright (C) 2016-2018 CERN. +# +# Invenio is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. +"""Add table for tracking acitivities.""" + +import sqlalchemy as sa +from alembic import op + +from cap.types import json_type + +# revision identifiers, used by Alembic. +revision = 'bceec8433ecb' +down_revision = 'dd3ef5d1ac6f' +branch_labels = () +depends_on = None + + +def upgrade(): + """Upgrade database.""" + op.create_table( + 'activity', sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('verb', sa.Unicode(length=255), nullable=True), + sa.Column('transaction_id', sa.BigInteger(), nullable=False), + sa.Column('data', json_type), + sa.Column('object_type', sa.String(length=255), nullable=True), + sa.Column('object_id', sa.BigInteger(), nullable=True), + sa.Column('object_tx_id', sa.BigInteger(), nullable=True), + sa.Column('target_type', sa.String(length=255), nullable=True), + sa.Column('target_id', sa.BigInteger(), nullable=True), + sa.Column('target_tx_id', sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_activity'))) + op.create_index(op.f('ix_activity_transaction_id'), + 'activity', ['transaction_id'], + unique=False) + + +def downgrade(): + """Downgrade database.""" + op.drop_index(op.f('ix_activity_transaction_id'), table_name='activity') + op.drop_table('activity') diff --git a/cap/config.py b/cap/config.py index 2ca7ad1292..cb3e226416 100644 --- a/cap/config.py +++ b/cap/config.py @@ -650,3 +650,5 @@ def _(x): LOGGING_SENTRY_CELERY = True """Configure Celery to send logging to Sentry.""" + +DB_EXTRA_PLUGINS = ['sqlalchemy_continuum.plugins.ActivityPlugin'] diff --git a/cap/modules/deposit/api.py b/cap/modules/deposit/api.py index dd5e3e970a..024dfc255d 100644 --- a/cap/modules/deposit/api.py +++ b/cap/modules/deposit/api.py @@ -24,7 +24,6 @@ """Deposit API.""" import copy -from functools import wraps from flask import current_app, request from flask_login import current_user @@ -45,13 +44,13 @@ from sqlalchemy.orm.exc import NoResultFound from werkzeug.local import LocalProxy +from cap.activities import register_activity from cap.modules.deposit.errors import DisconnectWebhookError, FileUploadError from cap.modules.deposit.validators import DepositValidator from cap.modules.experiments.permissions import exp_need_factory from cap.modules.records.api import CAPRecord from cap.modules.repos.errors import GitError from cap.modules.repos.factory import create_git_api -from cap.modules.repos.models import GitWebhook, GitWebhookSubscriber from cap.modules.repos.tasks import download_repo, download_repo_file from cap.modules.repos.utils import (create_webhook, disconnect_subscriber, parse_git_url) @@ -70,7 +69,7 @@ _datastore = LocalProxy(lambda: current_app.extensions['security'].datastore) -PRESERVE_FIELDS = ( +PROTECTED_FIELDS = ( '_deposit', '_buckets', '_files', @@ -78,7 +77,6 @@ '_access', '_user_edited', '_fetched_from', - 'general_title', '$schema', ) @@ -130,59 +128,6 @@ def build_deposit_schema(self, record): """Get schema path for deposit.""" return current_jsonschemas.path_to_url(self.schema.deposit_path) - def pop_from_data(method, fields=None): - """Remove fields from deposit data. - - :param fields: List of fields to remove (default: ``('_deposit',)``). - """ - fields = fields or ( - '_deposit', - '_access', - '_experiment', - '_fetched_from', - '_user_edited', - 'general_title', - '$schema', - ) - - @wraps(method) - def wrapper(self, *args, **kwargs): - """Check current deposit status.""" - for field in fields: - if field in args[0]: - args[0].pop(field) - - return method(self, *args, **kwargs) - - return wrapper - - def pop_from_data_patch(method, fields=None): - """Remove fields from deposit data. - - :param fields: List of fields to remove (default: ``('_deposit',)``). - """ - fields = fields or ( - '/_deposit', - '/_access', - '/_files', - '/_experiment', - '/_fetched_from', - '/_user_edited', - '/$schema', - ) - - @wraps(method) - def wrapper(self, *args, **kwargs): - """Check current deposit status.""" - for field in fields: - for k, patch in enumerate(args[0]): - if field == patch.get("path", None): - del args[0][k] - - return method(self, *args, **kwargs) - - return wrapper - @mark_as_action def permissions(self, pid=None): """Permissions action. @@ -196,9 +141,7 @@ def permissions(self, pid=None): }] """ with AdminDepositPermission(self).require(403): - data = request.get_json() - return self.edit_permissions(data) @mark_as_action @@ -352,16 +295,25 @@ def edit(self, *args, **kwargs): return self - @pop_from_data def update(self, *args, **kwargs): """Update deposit.""" with UpdateDepositPermission(self).require(403): + # filter out protected fields + for field in PROTECTED_FIELDS: + if field in args[0]: + args[0].pop(field) + super(CAPDeposit, self).update(*args, **kwargs) - @pop_from_data_patch def patch(self, *args, **kwargs): """Patch deposit.""" with UpdateDepositPermission(self).require(403): + # filter out protected fields + for field in ('/' + field for field in PROTECTED_FIELDS): + for patch in args[0]: + if patch.get("path", "").startswith(field): + args[0].remove(patch) + return super(CAPDeposit, self).patch(*args, **kwargs) def edit_permissions(self, data): @@ -427,7 +379,7 @@ def edit_permissions(self, data): return self - @preserve(result=False, fields=PRESERVE_FIELDS) + @preserve(result=False, fields=PROTECTED_FIELDS) def clear(self, *args, **kwargs): """Clear only drafts.""" super(CAPDeposit, self).clear(*args, **kwargs) @@ -602,9 +554,14 @@ def create(cls, data, id_=None, owner=current_user): deposit._create_buckets() deposit._set_experiment() deposit._init_owner_permissions(owner) - deposit.commit() + register_activity( + 'create deposit', + data=data, + object=deposit.model, + ) + return deposit @classmethod diff --git a/requirements-local-forks.txt b/requirements-local-forks.txt index a525e1e79e..fe7d219cca 100644 --- a/requirements-local-forks.txt +++ b/requirements-local-forks.txt @@ -1,3 +1,4 @@ +-e git+git://github.com/annatrz/invenio-db@master#egg=invenio-db[postgresql,versioning] -e git+git://github.com/annatrz/invenio-deposit.git#egg=invenio-deposit # -e git+git://github.com/reanahub/reana-client.git@master#egg=reana-client # -e git+git://github.com/reanahub/reana-commons.git@master#egg=reana-commons diff --git a/requirements.txt b/requirements.txt index 0d6edd8aa4..f17df6e938 100644 --- a/requirements.txt +++ b/requirements.txt @@ -74,7 +74,7 @@ invenio-config==1.0.0 # via invenio, invenio-app git+git://github.com/annatrz/invenio-deposit.git#egg=invenio-deposit git+git://github.com/reanahub/reana-client.git@master#egg=reana-client git+git://github.com/reanahub/reana-commons.git@master#egg=reana-commons -invenio-db[postgresql,versioning]==1.0.1 # via invenio, invenio-accounts-rest, invenio-admin +git+git://github.com/annatrz/invenio-db@master#egg=invenio-db[postgresql,versioning] invenio-formatter==1.0.1 # via invenio invenio-i18n==1.0.0 # via invenio, invenio-accounts, invenio-theme invenio-indexer==1.0.1 # via invenio, invenio-records-rest