diff --git a/Dockerfile b/Dockerfile index 12297a0d3c8..0b084061a32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -145,6 +145,7 @@ COPY ./admin/static/ ./admin/static/ COPY ./addons/bitbucket/static/ ./addons/bitbucket/static/ COPY ./addons/box/static/ ./addons/box/static/ COPY ./addons/citations/static/ ./addons/citations/static/ +COPY ./addons/datasteward/static/ ./addons/datasteward/static/ COPY ./addons/dataverse/static/ ./addons/dataverse/static/ COPY ./addons/dropbox/static/ ./addons/dropbox/static/ COPY ./addons/dropboxbusiness/static/ ./addons/dropboxbusiness/static/ diff --git a/addons.json b/addons.json index d928039bf16..efa34ae7b0f 100644 --- a/addons.json +++ b/addons.json @@ -32,6 +32,7 @@ "binderhub", "onedrivebusiness", "metadata", + "datasteward", "onlyoffice" ], "addons_default": [ diff --git a/addons/datasteward/__init__.py b/addons/datasteward/__init__.py new file mode 100644 index 00000000000..255b29f0e90 --- /dev/null +++ b/addons/datasteward/__init__.py @@ -0,0 +1 @@ +default_app_config = 'addons.datasteward.apps.DataStewardAddonAppConfig' diff --git a/addons/datasteward/apps.py b/addons/datasteward/apps.py new file mode 100644 index 00000000000..230ed0f5a1c --- /dev/null +++ b/addons/datasteward/apps.py @@ -0,0 +1,28 @@ +import os + +from addons.base.apps import BaseAddonAppConfig + +HERE = os.path.dirname(os.path.abspath(__file__)) +TEMPLATE_PATH = os.path.join( + HERE, + 'templates' +) + + +class DataStewardAddonAppConfig(BaseAddonAppConfig): + name = 'addons.datasteward' + label = 'addons_datasteward' + full_name = 'DataSteward' + short_name = 'datasteward' + owners = ['user'] + configs = ['accounts'] + user_settings_template = os.path.join(TEMPLATE_PATH, 'datasteward_user_settings.mako') + + @property + def routes(self): + from .routes import api_routes + return [api_routes] + + @property + def user_settings(self): + return self.get_model('UserSettings') diff --git a/addons/datasteward/migrations/0001_initial.py b/addons/datasteward/migrations/0001_initial.py new file mode 100644 index 00000000000..4843e4b74c9 --- /dev/null +++ b/addons/datasteward/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.28 on 2023-02-23 03:25 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields +import osf.models.base +import osf.utils.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserSettings', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('_id', models.CharField(db_index=True, default=osf.models.base.generate_object_id, max_length=24, unique=True)), + ('is_deleted', models.BooleanField(default=False)), + ('deleted', osf.utils.fields.NonNaiveDateTimeField(blank=True, null=True)), + ('enabled', models.BooleanField(default=False)), + ('owner', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='addons_datasteward_user_settings', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model, osf.models.base.QuerySetExplainMixin), + ), + ] diff --git a/addons/datasteward/migrations/__init__.py b/addons/datasteward/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/addons/datasteward/models.py b/addons/datasteward/models.py new file mode 100644 index 00000000000..4f8262bac7f --- /dev/null +++ b/addons/datasteward/models.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from addons.base.models import (BaseUserSettings, ) +from django.db import models + + +class DataStewardProvider(object): + name = 'DataSteward' + short_name = 'datasteward' + + def __init__(self, account=None): + super(DataStewardProvider, self).__init__() # this does exactly nothing... + # provide an unauthenticated session by default + self.account = account + + def __repr__(self): + return '<{name}: {status}>'.format( + name=self.__class__.__name__, + status=self.account.provider_id if self.account else 'anonymous' + ) + + +class UserSettings(BaseUserSettings): + oauth_provider = DataStewardProvider + + enabled = models.BooleanField(default=False) diff --git a/addons/datasteward/routes.py b/addons/datasteward/routes.py new file mode 100644 index 00000000000..41c043c6964 --- /dev/null +++ b/addons/datasteward/routes.py @@ -0,0 +1,21 @@ +from framework.routing import Rule, json_renderer + +from . import views + +api_routes = { + 'rules': [ + Rule( + '/settings/datasteward/', + 'get', + views.datasteward_user_config_get, + json_renderer, + ), + Rule( + '/settings/datasteward/', + 'post', + views.datasteward_user_config_post, + json_renderer, + ), + ], + 'prefix': '/api/v1' +} diff --git a/addons/datasteward/static/comicon.png b/addons/datasteward/static/comicon.png new file mode 100644 index 00000000000..81ddf482840 Binary files /dev/null and b/addons/datasteward/static/comicon.png differ diff --git a/addons/datasteward/static/datasteward.css b/addons/datasteward/static/datasteward.css new file mode 100644 index 00000000000..6663ee62ea1 --- /dev/null +++ b/addons/datasteward/static/datasteward.css @@ -0,0 +1,7 @@ +.datasteward-checkbox { + float: right; +} + +.datasteward-indent { + padding-left: 28px; +} diff --git a/addons/datasteward/static/datastewardUserConfig.js b/addons/datasteward/static/datastewardUserConfig.js new file mode 100644 index 00000000000..0ec3c3fa313 --- /dev/null +++ b/addons/datasteward/static/datastewardUserConfig.js @@ -0,0 +1,263 @@ +/** +* Module that controls the DataSteward user settings. Includes Knockout view-model +* for syncing data. +*/ + +var ko = require('knockout'); +var $ = require('jquery'); +var Raven = require('raven-js'); +require('js/osfToggleHeight'); + +var osfHelpers = require('js/osfHelpers'); + +var $modal = $('#datastewardModal'); +var $resultModal = $('#datastewardResultModal'); +var _ = require('js/rdmGettext')._; + +function ViewModel(url) { + var self = this; + + self.properName = 'DataSteward'; + + // Whether the initial data has been loaded + self.is_waiting = ko.observable(false); + + // Checkbox value + self.addon_enabled = ko.observable(false); + + // Whether add-on change failed or not + self.change_add_on_failed = ko.observable(false); + + // List of skipped projects while disabling DataSteward add-on + self.skipped_projects = ko.observable([]); + + // Whether modal is closed by user or programmatically closed + self.dialog_closed_by_user = ko.observable(false); + + // Flashed messages + self.message = ko.observable(''); + self.messageClass = ko.observable('text-info'); + + // Modal hidden events + $modal.on("hidden.bs.modal", function () { + if (self.dialog_closed_by_user()) { + self.addon_enabled(!self.addon_enabled()); + self.dialog_closed_by_user(false); + } else { + self.is_waiting(false); + $resultModal.modal('show'); + } + self.changeMessage('',''); + }); + + $resultModal.on("hidden.bs.modal", function () { + if (self.change_add_on_failed()) { + self.addon_enabled(!self.addon_enabled()); + self.change_add_on_failed(false); + } + self.skipped_projects([]); + }); + + window.onclick = function(event) { + var modalContentElement = document.querySelector('#datastewardModal .modal-content'); + if (!modalContentElement.contains(event.target) && self.is_waiting() === false) { + $modal.modal('hide'); + self.dialog_closed_by_user(true); + } + } + /** Close confirm modal */ + self.clearModal = function() { + $modal.modal('hide'); + self.dialog_closed_by_user(true); + }; + + /** Close result modal */ + self.clearResultModal = function() { + $resultModal.modal('hide'); + } + + /** Enable add on */ + self.enableAddon = function() { + self.is_waiting(true); + var data = { + 'enabled': self.addon_enabled() + }; + $.ajax({ + url: url, + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(data) + }).done(function (response) { + self.dialog_closed_by_user(false); + $modal.modal('hide'); + }).fail(function (xhr, textStatus, error) { + if (xhr.status === 403) { + self.is_waiting(false); + self.changeMessage(_('You do not have permission to perform this action.'), 'text-danger'); + Raven.captureMessage(_('You do not have permission to perform this action.'), { + extra: { + url: url, + textStatus: textStatus, + error: error + } + }); + } else { + self.change_add_on_failed(true); + self.dialog_closed_by_user(false); + $modal.modal('hide'); + } + }); + }; + + /** Disable add on */ + self.disableAddon = function() { + self.is_waiting(true); + var data = { + 'enabled': self.addon_enabled() + }; + $.ajax({ + url: url, + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(data) + }).done(function (response) { + var skipped_projects = response.skipped_projects; + self.skipped_projects(skipped_projects); + self.dialog_closed_by_user(false); + $modal.modal('hide'); + }).fail(function (xhr, textStatus, error) { + self.is_waiting(false); + self.changeMessage(_('Cannot disable DataSteward add-on'), 'text-danger'); + Raven.captureMessage(_('Cannot disable DataSteward add-on'), { + extra: { + url: url, + textStatus: textStatus, + error: error + } + }); + }); + }; + + /** Change the flashed status message */ + self.changeMessage = function(text, css, timeout) { + self.message(text); + var cssClass = css || 'text-info'; + self.messageClass(cssClass); + if (timeout) { + // Reset message after timeout period + setTimeout(function() { + self.message(''); + self.messageClass('text-info'); + }, timeout); + } + }; + + // Update observables with data from the server + self.fetch = function() { + self.is_waiting(true); + $.ajax({ + url: url, + type: 'GET', + dataType: 'json' + }).done(function (response) { + var enabled = response.enabled; + self.addon_enabled(enabled); + self.is_waiting(false); + }).fail(function (xhr, textStatus, error) { + self.changeMessage(_('Cannot get DataSteward add-on settings'), 'text-danger'); + Raven.captureMessage(_('Cannot get DataSteward add-on settings'), { + extra: { + url: url, + textStatus: textStatus, + error: error + } + }); + }); + }; + + /** Open confirm modal */ + self.openModal = function() { + $modal.modal('show'); + }; + + /** Create CSV data and save it to client */ + self.clickCSV = function () { + var skipped_projects = self.skipped_projects(); + if (!skipped_projects) { + return; + } + + var rows = skipped_projects.map(function(project) { + return [ + project['guid'], + project['name'] + ]; + }); + + exportToCsv('skipped_projects.csv', rows); + } + + /** Convert data array to CSV line string */ + var processRow = function (row) { + var finalVal = ''; + for (var j = 0; j < row.length; j++) { + var innerValue = row[j] === null ? '' : row[j].toString(); + if (row[j] instanceof Date) { + // If item is a Date, its locale string + innerValue = row[j].toLocaleString(); + } + var result = innerValue.replace(/"/g, '""'); + if (result.search(/("|,|\n)/g) >= 0) + // If string has doublequote, comma or line break characters then wrap it in doublequotes + result = '"' + result + '"'; + if (j > 0) + finalVal += ','; + finalVal += result; + } + return finalVal + '\n'; + }; + + /** Export data to CSV file */ + var exportToCsv = function(filename, rows) { + var csvFile = ''; + rows.forEach(function(row) { + csvFile += processRow(row) + }); + + var blob = new Blob([csvFile], { type: 'text/csv;charset=utf-8;' }); + if (navigator.msSaveBlob) { + // For IE 10+ + navigator.msSaveBlob(blob, filename); + } else { + // For modern browsers + var link = document.createElement("a"); + if (link.download !== undefined) { + var download_url = URL.createObjectURL(blob); + link.setAttribute("href", download_url); + link.setAttribute("download", filename); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Release csv URL object + URL.revokeObjectURL(download_url); + } + } + } +} + +function DataStewardUserConfig(selector, url) { + // Initialization code + var self = this; + self.selector = selector; + self.url = url; + // On success, instantiate and bind the ViewModel + self.viewModel = new ViewModel(url); + osfHelpers.applyBindings(self.viewModel, self.selector); +} + +module.exports = { + DataStewardViewModel: ViewModel, + DataStewardUserConfig: DataStewardUserConfig +}; diff --git a/addons/datasteward/static/user-cfg.js b/addons/datasteward/static/user-cfg.js new file mode 100644 index 00000000000..abb2f269b54 --- /dev/null +++ b/addons/datasteward/static/user-cfg.js @@ -0,0 +1,12 @@ +require('./datasteward.css'); +var $osf = require('js/osfHelpers'); +var DataStewardViewModel = require('./datastewardUserConfig.js').DataStewardViewModel; + +// Endpoint for DataSteward user settings +var url = '/api/v1/settings/datasteward/'; + +var datastewardViewModel = new DataStewardViewModel(url); +$osf.applyBindings(datastewardViewModel, '#datastewardAddonScope'); + +// Load initial DataSteward data +datastewardViewModel.fetch(); diff --git a/addons/datasteward/templates/datasteward_modal.mako b/addons/datasteward/templates/datasteward_modal.mako new file mode 100644 index 00000000000..5e812752fb9 --- /dev/null +++ b/addons/datasteward/templates/datasteward_modal.mako @@ -0,0 +1,122 @@ +## -*- coding: utf-8 -*- + diff --git a/addons/datasteward/templates/datasteward_result_modal.mako b/addons/datasteward/templates/datasteward_result_modal.mako new file mode 100644 index 00000000000..95fcbffa5d7 --- /dev/null +++ b/addons/datasteward/templates/datasteward_result_modal.mako @@ -0,0 +1,48 @@ + diff --git a/addons/datasteward/templates/datasteward_user_settings.mako b/addons/datasteward/templates/datasteward_user_settings.mako new file mode 100644 index 00000000000..1b3c2cb3212 --- /dev/null +++ b/addons/datasteward/templates/datasteward_user_settings.mako @@ -0,0 +1,15 @@ +
+ + <%include file="datasteward_modal.mako"/> + <%include file="datasteward_result_modal.mako"/> + +

+ + ${ addon_full_name } + + + +

+
diff --git a/addons/datasteward/tests/conftest.py b/addons/datasteward/tests/conftest.py new file mode 100644 index 00000000000..da9f243685b --- /dev/null +++ b/addons/datasteward/tests/conftest.py @@ -0,0 +1 @@ +from osf_tests.conftest import * # noqa diff --git a/addons/datasteward/tests/factories.py b/addons/datasteward/tests/factories.py new file mode 100644 index 00000000000..e9d9617f552 --- /dev/null +++ b/addons/datasteward/tests/factories.py @@ -0,0 +1,12 @@ +import factory +from factory.django import DjangoModelFactory +from osf_tests.factories import UserFactory + +from ..models import UserSettings + + +class UserSettingsFactory(DjangoModelFactory): + class Meta: + model = UserSettings + + owner = factory.SubFactory(UserFactory) diff --git a/addons/datasteward/tests/test_models.py b/addons/datasteward/tests/test_models.py new file mode 100644 index 00000000000..55077830efa --- /dev/null +++ b/addons/datasteward/tests/test_models.py @@ -0,0 +1,36 @@ +import unittest +import pytest + +from addons.datasteward.models import UserSettings, DataStewardProvider +from osf_tests.factories import AuthUserFactory + +pytestmark = pytest.mark.django_db + + +class TestDataStewardProvider(unittest.TestCase): + def setUp(self): + self.provider = DataStewardProvider() + + def test_account_none(self): + assert self.provider.account is None + + def test_oauth_provider_repr(self): + assert repr(self.provider) == '' + + +class TestUserSettings(unittest.TestCase): + def setUp(self): + self.user = AuthUserFactory() + self.user_settings = UserSettings(owner=self.user) + + def test_enabled_default_false(self): + assert self.user_settings.enabled is False + + def test_to_json(self): + expected_json = { + 'addon_short_name': 'datasteward', + 'addon_full_name': 'DataSteward', + 'has_auth': False, + 'nodes': [] + } + assert self.user_settings.to_json(self.user) == expected_json diff --git a/addons/datasteward/tests/test_views.py b/addons/datasteward/tests/test_views.py new file mode 100644 index 00000000000..5865e35be6c --- /dev/null +++ b/addons/datasteward/tests/test_views.py @@ -0,0 +1,467 @@ +import asyncio +import mock +import pytest +import unittest +from django.apps import apps + +from rest_framework import status as http_status +from nose.tools import (assert_equal, assert_true, assert_false) +from django.utils import timezone + +from addons.datasteward.tests.utils import DataStewardAddonTestCase +from addons.datasteward.views import ( + disable_datasteward_addon, + enable_datasteward_addon, + get_project_contributors, + get_node_settings_model, + bulk_create_contributors, + clear_permissions, + disconnect_addons_multiple_projects, + task_after_add_contributor, + task_after_update_contributor, + task_after_remove_contributor, + add_project_logs, + add_contributor_permission, + update_contributor_permission, + after_remove_contributor_permission, + BATCH_SIZE, +) +from framework.auth.core import Auth +from osf.exceptions import UserStateError, ValidationValueError +from osf.models import NodeLog, AbstractNode +from osf.models.contributor import Contributor +from osf.utils import permissions + +from osf_tests.factories import AuthUserFactory, ContributorFactory, InstitutionFactory, ProjectFactory, \ + osfstorage_settings +from tests.base import OsfTestCase +from website.util import api_url_for + +OSF_NODE = 'osf.node' + + +@pytest.mark.django_db +class TestDataStewardViews(DataStewardAddonTestCase, OsfTestCase, unittest.TestCase): + def setUp(self): + super(TestDataStewardViews, self).setUp() + self.new_user = AuthUserFactory(fullname='datasteward@osf', username='datasteward@osf.com') + self.new_user.save() + self.project = ProjectFactory(creator=self.new_user) + self.project.save() + self.auth = Auth(self.new_user) + + def set_datasteward_addon(self): + self.new_user.is_data_steward = True + self.new_user.save() + + self.settings = self.new_user.get_or_add_addon(self.ADDON_SHORT_NAME) + if not self.settings.enabled: + self.settings.enabled = True + self.settings.save() + self.new_user.save() + + def test_datasteward_user_config_get_forbidden(self): + url = api_url_for('datasteward_user_config_get') + self.new_user.is_data_steward = False + self.new_user.save() + + res = self.app.get(url, auth=self.new_user.auth, expect_errors=True) + + assert_equal(res.status_code, http_status.HTTP_403_FORBIDDEN) + assert_equal(res.json, {}) + + def test_datasteward_user_config_get_false_by_addon_settings_not_found(self): + url = api_url_for('datasteward_user_config_get') + self.new_user.is_data_steward = True + self.new_user.save() + + res = self.app.get(url, auth=self.new_user.auth) + + assert_equal(res.status_code, http_status.HTTP_200_OK) + assert_false(res.json.get('enabled')) + + def test_datasteward_user_config_get_true(self): + url = api_url_for('datasteward_user_config_get') + self.set_datasteward_addon() + + res = self.app.get(url, auth=self.new_user.auth) + + assert_equal(res.status_code, http_status.HTTP_200_OK) + assert_true(res.json.get('enabled')) + + def test_datasteward_user_config_post_bad_request(self): + url = api_url_for('datasteward_user_config_post') + res = self.app.post_json(url, {}, auth=self.new_user.auth, expect_errors=True) + assert_equal(res.status_code, http_status.HTTP_400_BAD_REQUEST) + + def test_datasteward_user_config_post_forbidden(self): + url = api_url_for('datasteward_user_config_post') + self.new_user.is_data_steward = False + self.new_user.save() + + res = self.app.post_json(url, {'enabled': True}, auth=self.new_user.auth, expect_errors=True) + assert_equal(res.status_code, http_status.HTTP_403_FORBIDDEN) + + @mock.patch('addons.datasteward.views.disable_datasteward_addon') + def test_datasteward_user_config_post_enabled_is_false_and_skipped_projects_is_none(self, mock_disable_addon): + self.set_datasteward_addon() + url = api_url_for('datasteward_user_config_post') + mock_disable_addon.return_value = None + + res = self.app.post_json(url, {'enabled': False}, auth=self.new_user.auth, expect_errors=True) + assert_equal(res.status_code, http_status.HTTP_500_INTERNAL_SERVER_ERROR) + + @mock.patch('addons.datasteward.views.disable_datasteward_addon') + def test_datasteward_user_config_post_enabled_is_false_and_skipped_projects_is_not_none(self, mock_disable_addon): + self.set_datasteward_addon() + url = api_url_for('datasteward_user_config_post') + project = ProjectFactory.build() + mock_disable_addon.return_value = [project] + + res = self.app.post_json(url, {'enabled': False}, auth=self.new_user.auth, status=http_status.HTTP_200_OK) + + assert_equal(res.status_code, http_status.HTTP_200_OK) + assert_equal(res.json.get('skipped_projects')[0].get('name'), project.title) + + @mock.patch('addons.datasteward.views.enable_datasteward_addon') + def test_datasteward_user_config_post_enabled_is_true_and_enable_addon_result_is_false(self, mock_enable_addon): + self.set_datasteward_addon() + url = api_url_for('datasteward_user_config_post') + mock_enable_addon.return_value = False + + res = self.app.post_json(url, {'enabled': True}, auth=self.new_user.auth, status=http_status.HTTP_500_INTERNAL_SERVER_ERROR) + assert_equal(res.status_code, http_status.HTTP_500_INTERNAL_SERVER_ERROR) + + @mock.patch('addons.datasteward.views.enable_datasteward_addon') + def test_datasteward_user_config_post_enabled_is_true_and_enable_addon_result_is_true(self, mock_enable_addon): + self.set_datasteward_addon() + url = api_url_for('datasteward_user_config_post') + mock_enable_addon.return_value = True + + res = self.app.post_json(url, {'enabled': True}, auth=self.new_user.auth) + assert_equal(res.status_code, http_status.HTTP_200_OK) + + def test_enable_datasteward_addon_no_affiliated_institutions(self): + result = enable_datasteward_addon(self.auth) + + assert_false(result) + + def test_enable_datasteward_addon_update_not_admin_permission_contributor_to_admin_permission_success(self): + user = AuthUserFactory(fullname='enabledatasteward@osf', username='enabledatasteward@osf.com') + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + user.affiliated_institutions.add(institution) + user.save() + + kwargs = node.contributor_kwargs + kwargs['_order'] = 0 + new_contributor = ContributorFactory(**kwargs) + new_contributor.user = user + new_contributor.is_data_steward = False + new_contributor.visible = True + new_contributor.save() + + result = enable_datasteward_addon(Auth(user)) + + updated_contributor = Contributor.objects.filter(node=node, user=user) + assert_true(updated_contributor.exists() and updated_contributor.first().permission == permissions.ADMIN) + assert_true(result) + + def test_enable_datasteward_addon_add_contributor_success(self): + user = AuthUserFactory(fullname='enabledatasteward@osf', username='enabledatasteward@osf.com') + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + user.affiliated_institutions.add(institution) + user.save() + + result = enable_datasteward_addon(Auth(user)) + + updated_contributor = Contributor.objects.filter(node=node, user=user) + assert_true(updated_contributor.exists() and updated_contributor.first().permission == permissions.ADMIN) + assert_true(result) + + def test_enable_datasteward_addon_add_contributor_user_is_disabled(self): + user = AuthUserFactory(fullname='enabledatasteward@osf', username='enabledatasteward@osf.com') + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + user.affiliated_institutions.add(institution) + user.date_disabled = timezone.now() + user.save() + + with self.assertRaises(ValidationValueError): + enable_datasteward_addon(Auth(user)) + + def test_enable_datasteward_addon_add_contributor_user_is_not_registered(self): + user = AuthUserFactory(fullname='enabledatasteward@osf', username='enabledatasteward@osf.com') + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + user.affiliated_institutions.add(institution) + user.is_registered = False + user.save() + + with self.assertRaises(UserStateError): + enable_datasteward_addon(Auth(user)) + + def test_disable_datasteward_addon_affiliated_institutions_is_empty(self): + result = disable_datasteward_addon(self.auth) + + assert_false(result) + + def test_disable_datasteward_addon_skip_project_has_only_one_admin(self): + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + self.new_user.affiliated_institutions.add(institution) + self.new_user.save() + + contributor = Contributor.objects.filter(user=self.new_user, node=node).first() + contributor.is_data_steward = True + contributor.data_steward_old_permission = permissions.READ + contributor.save() + result = disable_datasteward_addon(self.auth) + + assert_true(len(result) == 1) + + def test_disable_datasteward_addon_update_permission_success(self): + user = AuthUserFactory(fullname='disabledatasteward@osf', username='disabledatasteward@osf.com') + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + self.new_user.affiliated_institutions.add(institution) + self.new_user.save() + user.affiliated_institutions.add(institution) + user.save() + + contributor = Contributor.objects.filter(user=self.new_user, node=node).first() + contributor.is_data_steward = False + contributor.save() + + kwargs = node.contributor_kwargs + kwargs['_order'] = 0 + new_contributor = ContributorFactory(**kwargs) + new_contributor.user = user + new_contributor.is_data_steward = True + new_contributor.visible = True + new_contributor.data_steward_old_permission = permissions.READ + new_contributor.save() + + node.add_permission(user, permissions.ADMIN, save=False) + + result = disable_datasteward_addon(Auth(user)) + + assert_false(result) + + def test_disable_datasteward_addon_contributor_is_data_steward_is_False_skip_project(self): + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + self.new_user.affiliated_institutions.add(institution) + self.new_user.save() + + contributor = Contributor.objects.filter(user=self.new_user, node=node).first() + contributor.is_data_steward = False + contributor.save() + + result = disable_datasteward_addon(self.auth) + + assert_false(result) + + def test_disable_datasteward_addon_remove_contributor_skip_project(self): + user = AuthUserFactory(fullname='disabledatasteward@osf', username='disabledatasteward@osf.com') + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + self.new_user.affiliated_institutions.add(institution) + self.new_user.save() + user.affiliated_institutions.add(institution) + user.save() + + contributor = Contributor.objects.filter(user=self.new_user, node=node).first() + contributor.visible = False + contributor.save() + + kwargs = node.contributor_kwargs + kwargs['_order'] = 0 + new_contributor = ContributorFactory(**kwargs) + new_contributor.user = user + new_contributor.is_data_steward = True + new_contributor.visible = True + new_contributor.save() + + node.add_permission(user, permissions.ADMIN, save=False) + + result = disable_datasteward_addon(Auth(user)) + + assert_true(len(result) == 1) + + def test_disable_datasteward_addon_remove_contributor_success(self): + user = AuthUserFactory(fullname='disabledatasteward@osf', username='disabledatasteward@osf.com') + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + self.new_user.affiliated_institutions.add(institution) + self.new_user.save() + user.affiliated_institutions.add(institution) + user.unclaimed_records = {} + user.unclaimed_records[node._id] = {'name': node.title} + user.save() + + contributor = Contributor.objects.filter(user=self.new_user, node=node).first() + contributor.visible = True + contributor.save() + + kwargs = node.contributor_kwargs + kwargs['_order'] = 0 + new_contributor = ContributorFactory(**kwargs) + new_contributor.user = user + new_contributor.is_data_steward = True + new_contributor.visible = True + new_contributor.save() + + node.add_permission(user, permissions.ADMIN, save=False) + + result = disable_datasteward_addon(Auth(user)) + + assert_false(result) + + def test_disable_datasteward_addon_skip_project_not_have_user_as_contributor(self): + user = AuthUserFactory(fullname='disabledatasteward@osf', username='disabledatasteward@osf.com') + institution = InstitutionFactory() + node = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + institution.nodes.add(node) + institution.save() + self.new_user.affiliated_institutions.add(institution) + self.new_user.save() + user.affiliated_institutions.add(institution) + user.save() + + result = disable_datasteward_addon(Auth(user)) + + assert_false(result) + + def test_get_project_contributors(self): + node1 = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + node2 = ProjectFactory(creator=self.new_user, type=OSF_NODE, is_deleted=False) + contributor1 = ContributorFactory() + contributor1.user = self.new_user + contributor1.node = node1 + contributor2 = ContributorFactory() + contributor2.user = self.new_user + contributor2.node = node2 + contributors = [contributor1, contributor2] + + item1, item2, item3 = get_project_contributors(contributors, self.new_user, node1) + assert item1 == [contributor2] + assert item2 == [contributor1] + assert item3 == contributor1 + + def test_get_node_settings_model(self): + setting = get_node_settings_model(apps.get_app_config('addons_datasteward')) + assert setting is None + setting = get_node_settings_model(osfstorage_settings) + assert setting == osfstorage_settings.node_settings + + def test_bulk_create_contributors(self): + with mock.patch.object(Contributor.objects, 'bulk_create') as mock_bulk_create: + bulk_create_contributors(range(1, BATCH_SIZE + 1)) + assert mock_bulk_create.called + + def test_clear_permissions(self): + project1 = ProjectFactory() + project2 = ProjectFactory() + project1.add_permission(self.new_user, permissions.ADMIN, save=False) + project2.add_permission(self.new_user, permissions.READ, save=False) + projects = [project1, project2] + + clear_permissions(projects, self.new_user) + assert project1.get_permissions(self.new_user) == [] + assert project2.get_permissions(self.new_user) == [] + + def test_disconnect_addons_multiple_projects(self): + projects = [ProjectFactory(), ProjectFactory()] + with mock.patch('addons.datasteward.views.bulk_update') as mock_bulk_update: + disconnect_addons_multiple_projects(projects, self.new_user) + assert mock_bulk_update.called + + def test_add_project_logs(self): + project1 = ProjectFactory() + project2 = ProjectFactory() + project1.add_permission(self.new_user, permissions.ADMIN, save=False) + project2.add_permission(self.new_user, permissions.ADMIN, save=False) + projects = [project1, project2] + + add_project_logs(projects, self.new_user, NodeLog.CONTRIB_ADDED) + assert NodeLog.objects.filter(node__in=projects, action=NodeLog.CONTRIB_ADDED).count() == 2 + + def test_add_contributor_permission(self): + new_user = AuthUserFactory() + new_auth = Auth(new_user) + with mock.patch('addons.datasteward.views.enqueue_postcommit_task') as mock_task: + with mock.patch.object(AbstractNode, 'update_or_enqueue_on_resource_updated') as mock_resource_updated: + self.project.set_identifier_value('doi', 'FK424601') + loop = asyncio.new_event_loop() + coro = loop.create_task(add_contributor_permission(self.project, new_auth)) + loop.run_until_complete(asyncio.wait([coro])) + loop.close() + + assert mock_task.called + assert mock_resource_updated.called + assert permissions.ADMIN in self.project.get_permissions(new_user) + + def test_update_contributor_permission(self): + new_user = AuthUserFactory() + new_auth = Auth(new_user) + with mock.patch('addons.datasteward.views.enqueue_postcommit_task') as mock_task: + loop = asyncio.new_event_loop() + coro = loop.create_task(update_contributor_permission(self.project, new_auth, permissions.READ)) + loop.run_until_complete(asyncio.wait([coro])) + loop.close() + assert mock_task.called + assert self.project.get_permissions(new_user) == [permissions.READ] + + def test_after_remove_contributor_permission(self): + new_user = AuthUserFactory() + new_auth = Auth(new_user) + with mock.patch('addons.datasteward.views.enqueue_postcommit_task') as mock_task: + with mock.patch.object(AbstractNode, 'update_or_enqueue_on_resource_updated') as mock_resource_updated: + self.project.set_identifier_value('doi', 'FK424601') + loop = asyncio.new_event_loop() + coro = loop.create_task(after_remove_contributor_permission(self.project, new_auth)) + loop.run_until_complete(asyncio.wait([coro])) + loop.close() + assert mock_task.called + assert mock_resource_updated.called + + def test_task_after_add_contributor(self): + with mock.patch('addons.datasteward.views.project_signals.contributors_updated') as mock_contributors_updated: + with mock.patch('addons.datasteward.views.project_signals.contributor_added') as mock_contributor_added: + task_after_add_contributor(self.project.id, self.new_user.id) + assert mock_contributors_updated.send.called + assert mock_contributor_added.send.called + + def test_task_after_update_contributor(self): + with mock.patch('addons.datasteward.views.project_signals.write_permissions_revoked') as mock_write_permissions_revoked: + with mock.patch('addons.datasteward.views.project_signals.contributors_updated') as mock_contributors_updated: + task_after_update_contributor(self.project.id, permissions.READ) + assert mock_write_permissions_revoked.send.called + assert mock_contributors_updated.send.called + + def test_task_after_remove_contributor(self): + with mock.patch('addons.datasteward.views.remove_contributor_from_subscriptions') as mock_remove_contributor_from_subscriptions: + with mock.patch('addons.datasteward.views.project_signals.contributors_updated') as mock_contributors_updated: + task_after_remove_contributor(self.project.id, self.new_user.id) + assert mock_remove_contributor_from_subscriptions.called + assert mock_contributors_updated.send.called diff --git a/addons/datasteward/tests/utils.py b/addons/datasteward/tests/utils.py new file mode 100644 index 00000000000..b6821f6e61c --- /dev/null +++ b/addons/datasteward/tests/utils.py @@ -0,0 +1,13 @@ +from addons.base.tests.base import AddonTestCase +from addons.datasteward.models import DataStewardProvider + +class DataStewardAddonTestCase(AddonTestCase): + ADDON_SHORT_NAME = 'datasteward' + Provider = DataStewardProvider + OWNERS = ['user'] + + def set_node_settings(self, settings): + pass + + def set_user_settings(self, settings): + pass diff --git a/addons/datasteward/views.py b/addons/datasteward/views.py new file mode 100644 index 00000000000..8da116989db --- /dev/null +++ b/addons/datasteward/views.py @@ -0,0 +1,468 @@ +"""Views for the add-on's user settings page.""" +import asyncio +import logging +from itertools import islice + +from bulk_update.helper import bulk_update +# -*- coding: utf-8 -*- +from django.apps import apps +from django.db import transaction +from django.db.models import Max, Q +from django.db.models.functions import Coalesce +from flask import request +from rest_framework import status as http_status + +from addons.osfstorage.listeners import checkin_files_task +from framework.auth import Auth +from framework.auth.decorators import must_be_logged_in +from framework.celery_tasks import app +from framework.postcommit_tasks.handlers import enqueue_postcommit_task +from osf.exceptions import UserStateError, ValidationValueError +from osf.models import Contributor, OSFUser +from osf.models.node import AbstractNode +from osf.models.nodelog import NodeLog +from osf.utils.permissions import ADMIN, READ +from website import language +from website.notifications.utils import remove_contributor_from_subscriptions +from website.project import signals as project_signals + +logger = logging.getLogger(__name__) + +SHORT_NAME = 'datasteward' +OSF_NODE = 'osf.node' +BATCH_SIZE = 1000 + + +@must_be_logged_in +def datasteward_user_config_get(auth, **kwargs): + """View for getting a JSON representation of DataSteward user settings""" + user = auth.user + addon_user_settings = user.get_addon(SHORT_NAME) + addon_user_settings_enabled = addon_user_settings.enabled if addon_user_settings else False + + # If user is not a data steward and does not have add-on enabled before, return HTTP 403 + if not user.is_data_steward and not addon_user_settings_enabled: + return {}, http_status.HTTP_403_FORBIDDEN + + return { + 'enabled': addon_user_settings_enabled, + }, http_status.HTTP_200_OK + + +@must_be_logged_in +def datasteward_user_config_post(auth, **kwargs): + """View for post DataSteward user settings with enabled value""" + data = request.get_json() + enabled = data.get('enabled', None) + if enabled is None or not isinstance(enabled, bool): + # If request's 'enabled' field is not valid, return HTTP 400 + return {}, http_status.HTTP_400_BAD_REQUEST + + user = auth.user + # If user is not a DataSteward when enabling DataSteward add-on, return HTTP 403 + if not user.is_data_steward and enabled: + return {}, http_status.HTTP_403_FORBIDDEN + + # Update add-on user settings + addon_user_settings = user.get_addon(SHORT_NAME) + addon_user_settings.enabled = enabled + addon_user_settings.save() + + if enabled: + # Enable DataSteward addon process + enable_result = enable_datasteward_addon(auth) + + if not enable_result: + return {}, http_status.HTTP_500_INTERNAL_SERVER_ERROR + + return {}, http_status.HTTP_200_OK + else: + # Disable DataSteward addon process + skipped_projects = disable_datasteward_addon(auth) + + if skipped_projects is None: + return {}, http_status.HTTP_500_INTERNAL_SERVER_ERROR + + response_body = { + 'skipped_projects': [ + { + 'guid': project._id, + 'name': project.title + } + for project in skipped_projects + ] + } + + return response_body, http_status.HTTP_200_OK + + +@transaction.atomic +def enable_datasteward_addon(auth, is_authenticating=False, **kwargs): + """Start enable DataSteward add-on process""" + # Check if user has any affiliated institutions + affiliated_institutions = auth.user.affiliated_institutions.all() + if not affiliated_institutions: + return False + try: + user = auth.user.merged_by if auth.user.is_merged else auth.user + projects = AbstractNode.objects.filter(type=OSF_NODE, affiliated_institutions__in=affiliated_institutions, is_deleted=False) + contributors = Contributor.objects.filter(node__in=projects) + + add_contributor_list = [] + update_contributor_list = [] + update_permission_project_list = [] + for project in projects: + related_contributors = contributors.filter(node=project) + if related_contributors.filter(user=user).exists(): + contributor = related_contributors.filter(user=user).first() + + # check if need to save old permission + if not contributor.is_data_steward: + contributor.data_steward_old_permission = contributor.permission + contributor.is_data_steward = True + update_contributor_list.append(contributor) + + # check if need to update permission + if contributor.permission != ADMIN: + update_permission_project_list.append(project) + else: + # add contributor + kwargs = project.contributor_kwargs + kwargs['_order'] = related_contributors.aggregate(**{'_order__max': Coalesce(Max('_order'), -1)}).get('_order__max') + 1 + new_contributor = Contributor(**kwargs) + new_contributor.user = user + new_contributor.is_data_steward = True + new_contributor.visible = True + add_contributor_list.append((project, new_contributor)) + + # add contributor + if add_contributor_list: + if user.is_disabled: + raise ValidationValueError('Deactivated users cannot be added as contributors.') + + if not user.is_registered and not user.unclaimed_records: + raise UserStateError('This contributor cannot be added. If the problem persists please report it ' + 'to ' + language.SUPPORT_LINK) + + new_contributors = [new_contributor for _, new_contributor in add_contributor_list] + added_projects = [project for project, _ in add_contributor_list] + bulk_create_contributors(new_contributors) + + # Bulk create NodeLog + add_project_logs(added_projects, user, NodeLog.CONTRIB_ADDED) + + # send signal. + loop = asyncio.new_event_loop() + coroutines = [loop.create_task(add_contributor_permission(project=project, auth=auth)) for project, contributor in add_contributor_list] + loop.run_until_complete(asyncio.wait(coroutines)) + loop.close() + + # save contributor's old permission + if update_contributor_list: + bulk_update(update_contributor_list, batch_size=BATCH_SIZE) + + # update contributor permission + if update_permission_project_list: + # set permission and send signal + loop = asyncio.new_event_loop() + coroutines = [loop.create_task(update_contributor_permission(project=project, auth=auth)) for project in update_permission_project_list] + loop.run_until_complete(asyncio.wait(coroutines)) + loop.close() + except Exception as e: + # If error is raised while running on "Configure add-on accounts" screen, raise error + # Otherwise, do nothing + logger.error('Project {}: error raised while enabling DataSteward add-on with message "{}"'.format(project._id, e)) + if not is_authenticating: + raise e + return True + + +@transaction.atomic +def disable_datasteward_addon(auth): + """Start disable DataSteward add-on process""" + # Check if user has any affiliated institutions + affiliated_institutions = auth.user.affiliated_institutions.all() + if not affiliated_institutions: + return None + + user = auth.user + skipped_projects = [] + + # Query projects + projects = AbstractNode.objects.filter(type=OSF_NODE, affiliated_institutions__in=affiliated_institutions, is_deleted=False) + + # Query admin contributors + contributors = Contributor.objects.filter(node__in=projects, user__is_active=True).\ + filter(Q(is_data_steward=True) | Q(user__groups__name__in=[project.format_group(ADMIN) for project in projects])).distinct() + all_project_contributors = [] + user_project_id_list = [] + for contributor in contributors: + all_project_contributors.append(contributor) + if contributor.node.id not in user_project_id_list and contributor.user.id == user.id: + user_project_id_list.append(contributor.node.id) + + # Filter out projects that does not have user as contributor. + if len(user_project_id_list) != projects.count(): + projects = projects.filter(id__in=user_project_id_list) + + update_permission_list = [] + bulk_update_contributors = [] + bulk_delete_contributor_id_list = [] + remove_permission_projects = [] + update_user = False + for project in projects: + all_project_contributors, project_admin_contributors, contributor = get_project_contributors(all_project_contributors, user, project) + + if not contributor or not contributor.is_data_steward: + continue + + if contributor.data_steward_old_permission is not None: + # Contributor has old permission before enabling DataSteward add-on + if len(project_admin_contributors) <= 1: + # has only one admin + if ADMIN != contributor.data_steward_old_permission: + # if update to other permission than ADMIN then skip. + # else do nothing. + error_msg = '{} is the only admin.'.format(user.fullname) + logger.warning('Project {}: error raised while disabling DataSteward add-on with message "{}"'.format(project._id, error_msg)) + skipped_projects.append(project) + else: + contributor.is_data_steward = False + contributor.data_steward_old_permission = None + bulk_update_contributors.append(contributor) + else: + update_permission_list.append((project, contributor.data_steward_old_permission)) + contributor.is_data_steward = False + contributor.data_steward_old_permission = None + bulk_update_contributors.append(contributor) + else: + # Contributor does not have old permission before enabling DataSteward add-on + # Get project's admin contributors that is not current user + not_current_user_admins = [contributor for contributor in project_admin_contributors if contributor.user.id != user.id and contributor.visible is True] + if not not_current_user_admins: + # If user is the only visible contributor then skip + logger.warning('Cannot remove user from project {}'.format(project._id)) + skipped_projects.append(project) + else: + # remove unclaimed record if necessary + if project._id in user.unclaimed_records: + del user.unclaimed_records[project._id] + update_user = True + bulk_delete_contributor_id_list.append(contributor.id) + remove_permission_projects.append(project) + + if bulk_update_contributors: + # update to DB + bulk_update(bulk_update_contributors, update_fields=['is_data_steward', 'data_steward_old_permission'], batch_size=BATCH_SIZE) + + # update contributor permission + if update_permission_list: + # set permission and send signal + loop = asyncio.new_event_loop() + coroutines = [loop.create_task(update_contributor_permission(project=project, auth=auth, permission=contributor_old_permission)) for + project, contributor_old_permission in update_permission_list] + loop.run_until_complete(asyncio.wait(coroutines)) + loop.close() + + # remove contributor + if remove_permission_projects: + if update_user: + # save after delete unclaimed_records + user.save() + + # Delete contributors + Contributor.objects.filter(id__in=bulk_delete_contributor_id_list).delete() + + # Clear permission + clear_permissions(remove_permission_projects, user) + + # Add multiple logs for contributor removed event + add_project_logs(remove_permission_projects, user, NodeLog.CONTRIB_REMOVED) + + # Disconnect addons for multiple projects at once + disconnect_addons_multiple_projects(remove_permission_projects, user) + + # Run extra code after removing user from project + loop = asyncio.new_event_loop() + coroutines = [loop.create_task(after_remove_contributor_permission(project=project, auth=auth)) for project in remove_permission_projects] + loop.run_until_complete(asyncio.wait(coroutines)) + loop.close() + + return skipped_projects + + +def get_project_contributors(contributors, user, project): + """ Get project and contributor instances from user""" + new_list = [] + project_admin_contributors = [] + user_contributor = None + for contributor in contributors: + if contributor.node.id == project.id: + if contributor.permission == ADMIN: + project_admin_contributors.append(contributor) + + if contributor.user.id == user.id: + user_contributor = contributor + else: + new_list.append(contributor) + + return new_list, project_admin_contributors, user_contributor + + +def get_node_settings_model(config): + """ Get addon's node settings""" + try: + settings_model = getattr(config, '{}_settings'.format('node')) + except LookupError: + return None + return settings_model + + +def bulk_create_contributors(contributors, batch_size=BATCH_SIZE): + """ Bulk create multiple contributors for projects """ + it = iter(contributors) + while True: + batch = list(islice(it, batch_size)) + if not batch: + break + Contributor.objects.bulk_create(batch, batch_size) + + +def clear_permissions(projects, user): + """ Clear permission of multiple projects for user """ + project_group_names = [name for project in projects for name in project.group_names] + OSFUserGroup = apps.get_model('osf', 'osfuser_groups') + OSFUserGroup.objects.filter(osfuser_id=user.id, group__name__in=project_group_names).delete() + + +def disconnect_addons_multiple_projects(projects, user): + """ Disconnect addons for multiple projects at same time """ + ADDONS_AVAILABLE = sorted([config for config in apps.get_app_configs() if config.name.startswith('addons.') and + config.label != 'base'], key=lambda config: config.name) + project_ids = [project.id for project in projects if not project.is_contributor_or_group_member(user)] + + # If there is no add-on or project then returns + if not ADDONS_AVAILABLE or not project_ids: + return + + for config in ADDONS_AVAILABLE: + # Get addon's node settings model + node_settings_model = get_node_settings_model(config) + if not node_settings_model: + continue + + # Get project's node settings for each add-on + project_node_settings = node_settings_model.objects.filter(owner__id__in=project_ids) + + update_node_settings = False + for setting in project_node_settings: + if not hasattr(setting, 'user_settings'): + continue + + update_node_settings = True + if hasattr(setting.user_settings, 'oauth_grants'): + # Remove user's external account guid from node's oauth_grants + setting.user_settings.oauth_grants[setting.owner._id].pop(setting.external_account._id) + + # Disconnect user settings from node settings + setting.user_settings = None + + if update_node_settings: + # Bulk update multiple node settings + bulk_update(project_node_settings, batch_size=BATCH_SIZE) + + +def add_project_logs(projects, user, action): + """ Add multiple project activity logs for user """ + logs = [] + for project in projects: + params = project.log_params + params['contributors'] = [user._id] + params['node'] = params.get('node') or params.get('project') or project._id + original_node = project if project._id == params['node'] else AbstractNode.load(params.get('node')) + log = NodeLog( + action=action, user=user, foreign_user=None, + params=params, node=project, original_node=original_node + ) + logs.append(log) + + it = iter(logs) + while True: + batch = list(islice(it, BATCH_SIZE)) + if not batch: + break + NodeLog.objects.bulk_create(batch, BATCH_SIZE) + + +async def add_contributor_permission(project, auth): + """ Add user to project as administrator """ + project.add_permission(auth.user, ADMIN, save=False) + enqueue_postcommit_task(task_after_add_contributor, (project.id, auth.user.id,), {}, celery=True) + + # enqueue on_node_updated/on_preprint_updated to update DOI metadata when a contributor is added + if getattr(project, 'get_identifier_value', None) and project.get_identifier_value('doi'): + project.update_or_enqueue_on_resource_updated(auth.user._id, first_save=False, saved_fields=['contributors']) + + +async def update_contributor_permission(project, auth, permission=ADMIN): + """ Update exiting user of project """ + if not project.get_group(permission).user_set.filter(id=auth.user.id).exists(): + project.set_permissions(auth.user, permission, save=False) + permissions_changed = { + auth.user._id: permission + } + params = project.log_params + params['contributors'] = permissions_changed + project.add_log( + action=project.log_class.PERMISSIONS_UPDATED, + params=params, + auth=auth, + save=False + ) + enqueue_postcommit_task(task_after_update_contributor, (project.id, permission,), {}, celery=True) + + +async def after_remove_contributor_permission(project, auth): + # contributor_removed signal receiver function from addons/osfstorage/listeners.py + enqueue_postcommit_task(checkin_files_task, (project._id, auth.user._id,), {}, celery=True) + # Run background tasks to remove this user from project subscriptions + enqueue_postcommit_task(task_after_remove_contributor, (project.id, auth.user.id,), {}, celery=True) + + # enqueue on_node_updated/on_preprint_updated to update DOI metadata when a contributor is removed + if getattr(project, 'get_identifier_value', None) and project.get_identifier_value('doi'): + project.update_or_enqueue_on_resource_updated(auth.user._id, first_save=False, saved_fields=['contributors']) + + +# Task-related functions +@app.task +def task_after_add_contributor(node_id, user_id): + node = AbstractNode.objects.get(pk=node_id) + user = OSFUser.objects.get(pk=user_id) + auth = Auth(user=user) + + project_signals.contributors_updated.send(node) + if node._id and user: + project_signals.contributor_added.send(node, + contributor=user, + auth=auth, email_template='false', permissions=[ADMIN]) + + +@app.task +def task_after_update_contributor(node_id, permission): + node = AbstractNode.objects.get(pk=node_id) + if permission == READ: + project_signals.write_permissions_revoked.send(node) + + project_signals.contributors_updated.send(node) + + +@app.task +def task_after_remove_contributor(node_id, user_id): + node = AbstractNode.objects.get(pk=node_id) + user = OSFUser.objects.get(pk=user_id) + + # contributor_removed signal receiver function from website/notifications/utils.py + remove_contributor_from_subscriptions(node, user) + + # send contributors_updated signal + project_signals.contributors_updated.send(node) diff --git a/admin/base/settings/defaults.py b/admin/base/settings/defaults.py index f8808424cb3..9df605d4454 100644 --- a/admin/base/settings/defaults.py +++ b/admin/base/settings/defaults.py @@ -139,6 +139,7 @@ 'addons.ociinstitutions', 'addons.onedrivebusiness', 'addons.metadata', + 'addons.datasteward', ) MIGRATION_MODULES = { @@ -185,7 +186,8 @@ 'nextcloud', 'gitlab', 'onedrive', - 'iqbrims' + 'iqbrims', + 'datasteward' ] USE_TZ = True diff --git a/admin/rdm_addons/utils.py b/admin/rdm_addons/utils.py index 21ba003b211..11c094abd70 100644 --- a/admin/rdm_addons/utils.py +++ b/admin/rdm_addons/utils.py @@ -37,7 +37,7 @@ def get_addon_template_config(config, user): 'institution_settings_template': get_institusion_settings_template(config), 'is_enabled': user_addon is not None, 'addon_icon_url': reverse('addons:icon', args=[config.short_name, config.icon]), - 'is_supported_force_to_use': config.short_name not in UNSUPPORTED_FORCE_TO_USE_ADDONS, + 'is_supported_force_to_use': config.short_name not in [*UNSUPPORTED_FORCE_TO_USE_ADDONS, 'datasteward'], } ret.update(user_addon.to_json(user) if user_addon else {}) return ret diff --git a/admin/static/js/rdm_addons/rdm-addons-page.js b/admin/static/js/rdm_addons/rdm-addons-page.js index d6d8fa70db7..eb652cb5a75 100644 --- a/admin/static/js/rdm_addons/rdm-addons-page.js +++ b/admin/static/js/rdm_addons/rdm-addons-page.js @@ -60,12 +60,21 @@ $('.is_allowed input').on('change', function() { } else { var deletionKey = Math.random().toString(36).slice(-8); var id = addonName + "DeleteKey"; - bootbox.confirm({ - title: sprintf(_("Disallow %s?"),$osf.htmlEscape(addonFullName)), - message: sprintf(_("Are you sure you want to disallow the %1$s?
"),$osf.htmlEscape(addonFullName)) + + var message = sprintf(_("Are you sure you want to disallow the %1$s?
"),$osf.htmlEscape(addonFullName)) + sprintf(_("This will revoke access to %1$s for all projects using the accounts.

"),$osf.htmlEscape(addonFullName)) + sprintf(_("Type the following to continue: %1$s

"),$osf.htmlEscape(deletionKey)) + - "", + ""; + if (addonName === 'datasteward') { + message = sprintf(_("Are you sure you want to disallow the %1$s?
"),$osf.htmlEscape(addonFullName)) + + sprintf(_("This will not revoke access to %1$s for all projects using the accounts.
"),$osf.htmlEscape(addonFullName)) + + sprintf(_("But the accounts will not be able to see and change their %1$s settings until you reallow it.

"),$osf.htmlEscape(addonFullName)) + + sprintf(_("Type the following to continue: %1$s

"),$osf.htmlEscape(deletionKey)) + + "" + } + bootbox.confirm({ + title: sprintf(_("Disallow %s?"),$osf.htmlEscape(addonFullName)), + message: message, + backdrop: true, buttons: { cancel: { label: _('Cancel') diff --git a/api/base/settings/defaults.py b/api/base/settings/defaults.py index 781dbd6fb82..c84639f4bb8 100644 --- a/api/base/settings/defaults.py +++ b/api/base/settings/defaults.py @@ -126,6 +126,7 @@ 'addons.binderhub', 'addons.onedrivebusiness', 'addons.metadata', + 'addons.datasteward', 'addons.onlyoffice', ) diff --git a/api/institutions/authentication.py b/api/institutions/authentication.py index 4a6e5e8217e..69420dbadfc 100644 --- a/api/institutions/authentication.py +++ b/api/institutions/authentication.py @@ -12,9 +12,10 @@ from api.base.authentication import drf from api.base import exceptions, settings +from addons.datasteward.views import enable_datasteward_addon from framework import sentry -from framework.auth import get_or_create_user +from framework.auth import get_or_create_user, Auth from framework.auth.core import get_user from osf import features @@ -464,6 +465,24 @@ def get_next(obj, *args): user.save() update_default_storage(user) + # Update DataSteward status after every time user login + if entitlement and 'GakuNinRDMDataSteward' in entitlement: + if not user.is_data_steward: + # Set user.is_data_steward to True + user.is_data_steward = True + user.save() + + # Get user DataSteward add-on setings + addon_user_settings = user.get_addon('datasteward') + if addon_user_settings and addon_user_settings.enabled: + # If user enabled DataSteward add-on, start enable Datasteward add-on process + auth = Auth(user=user) + enable_datasteward_addon(auth, is_authenticating=True) + else: + # Set user.is_data_steward to False + user.is_data_steward = False + user.save() + # update every login. (for mAP API v1) init_cloud_gateway_groups(user, provider) diff --git a/api_tests/institutions/views/test_institution_auth.py b/api_tests/institutions/views/test_institution_auth.py index 0b1192c6bf0..d138452517b 100644 --- a/api_tests/institutions/views/test_institution_auth.py +++ b/api_tests/institutions/views/test_institution_auth.py @@ -36,6 +36,7 @@ def make_payload( jaOrganizationalUnitName='', organizationalUnit='', organizationName='', + entitlement='', edu_person_affiliation='', edu_person_scoped_affiliation='', edu_person_targeted_id='', @@ -68,6 +69,7 @@ def make_payload( 'jaOrganizationalUnitName': jaOrganizationalUnitName, 'organizationalUnitName': organizationalUnit, 'organizationName': organizationName, + 'entitlement': entitlement, 'eduPersonAffiliation': edu_person_affiliation, 'eduPersonScopedAffiliation': edu_person_scoped_affiliation, 'eduPersonTargetedID': edu_person_targeted_id, @@ -542,6 +544,49 @@ def test_authenticate_OrganizationalUnitName_is_valid( assert user assert user.jobs[0]['department'] == organizationnameunit + def test_authenticate_turn_datasteward_on(self, app, institution, url_auth_institution): + username = 'user_datasteward@osf.edu' + entitlement = 'GakuNinRDMDataSteward' + res = app.post( + url_auth_institution, + make_payload(institution, username, entitlement=entitlement) + ) + assert res.status_code == 204 + user = OSFUser.objects.filter(username=username).first() + assert user + assert user.is_data_steward is True + + def test_authenticate_turn_datasteward_off(self, app, institution, url_auth_institution): + username = 'user_datasteward@osf.edu' + entitlement = '' + res = app.post( + url_auth_institution, + make_payload(institution, username, entitlement=entitlement) + ) + assert res.status_code == 204 + user = OSFUser.objects.filter(username=username).first() + assert user + assert user.is_data_steward is False + + async def test_authenticate_enable_datasteward_addon(self, app, url_auth_institution, institution): + username = 'datasteward@osf.edu' + user = make_user(username, 'addon datasteward') + user.save() + + settings = user.get_or_add_addon('datasteward') + if not settings.enabled: + settings.enabled = True + settings.save() + user.save() + + with capture_signals() as mock_signals: + res = await app.post(url_auth_institution, make_payload(institution, username, entitlement='GakuNinRDMDataSteward')) + assert res.status_code == 204 + assert not mock_signals.signals_sent() + + user.reload() + assert user.is_data_steward is True + @mock.patch('api.institutions.authentication.login_by_eppn') def test_with_new_attribute(self, mock, app, institution, url_auth_institution): mock.return_value = True diff --git a/osf/migrations/0226_osfuser_is_data_steward.py b/osf/migrations/0226_osfuser_is_data_steward.py new file mode 100644 index 00000000000..c1d5a48888b --- /dev/null +++ b/osf/migrations/0226_osfuser_is_data_steward.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.28 on 2023-02-20 10:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0225_add_pattern_and_norm_to_registration_schema_block'), + ] + + operations = [ + migrations.AddField( + model_name='osfuser', + name='is_data_steward', + field=models.BooleanField(default=False), + ), + ] diff --git a/osf/migrations/0227_auto_20230222_1029.py b/osf/migrations/0227_auto_20230222_1029.py new file mode 100644 index 00000000000..d1fc49f3c05 --- /dev/null +++ b/osf/migrations/0227_auto_20230222_1029.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.28 on 2023-02-22 10:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0226_osfuser_is_data_steward'), + ('osf', '0226_alter_filelog_path'), + ] + + operations = [ + migrations.AddField( + model_name='contributor', + name='data_steward_old_permission', + field=models.CharField(choices=[('NULL', None), ('READ', 'read'), ('WRITE', 'write'), ('ADMIN', 'admin')], max_length=255, null=True), + ), + migrations.AddField( + model_name='contributor', + name='is_data_steward', + field=models.BooleanField(db_index=True, default=False), + ), + ] diff --git a/osf/models/contributor.py b/osf/models/contributor.py index 47317164082..6fa14ad01b0 100644 --- a/osf/models/contributor.py +++ b/osf/models/contributor.py @@ -5,6 +5,14 @@ from osf.utils import permissions +CONTRIBUTOR_PERMISSION_CHOICES = ( + ('NULL', None), + ('READ', 'read'), + ('WRITE', 'write'), + ('ADMIN', 'admin'), +) + + class AbstractBaseContributor(models.Model): objects = IncludeManager() @@ -33,6 +41,8 @@ def permission(self): class Contributor(AbstractBaseContributor): node = models.ForeignKey('AbstractNode', on_delete=models.CASCADE) + is_data_steward = models.BooleanField(default=False, db_index=True) + data_steward_old_permission = models.CharField(null=True, max_length=255, choices=CONTRIBUTOR_PERMISSION_CHOICES) @property def _id(self): diff --git a/osf/models/user.py b/osf/models/user.py index 2725f9360be..c2d1070cb71 100644 --- a/osf/models/user.py +++ b/osf/models/user.py @@ -450,6 +450,7 @@ class OSFUser(DirtyFieldsMixin, GuidMixin, BaseModel, AbstractBaseUser, Permissi is_active = models.BooleanField(default=False) is_staff = models.BooleanField(default=False) + is_data_steward = models.BooleanField(default=False) # ePPN, eduPersonTargetedID and isMemberOf from Shibboleth # for Cloud Gateway diff --git a/osf_tests/factories.py b/osf_tests/factories.py index c0aed5dccd4..b9f37e021ab 100644 --- a/osf_tests/factories.py +++ b/osf_tests/factories.py @@ -4,7 +4,7 @@ import datetime import mock from factory import SubFactory -from factory.fuzzy import FuzzyDateTime, FuzzyAttribute, FuzzyChoice +from factory.fuzzy import FuzzyDateTime, FuzzyAttribute, FuzzyChoice, FuzzyText from mock import patch, Mock import factory @@ -252,6 +252,12 @@ class NodeFactory(BaseNodeFactory): category = 'hypothesis' parent = factory.SubFactory(ProjectFactory) +class ContributorFactory(DjangoModelFactory): + user = factory.SubFactory(AuthUserFactory) + node = factory.SubFactory(ProjectFactory) + + class Meta: + model = models.Contributor class InstitutionFactory(DjangoModelFactory): name = factory.Faker('company') @@ -1145,7 +1151,7 @@ class BrandFactory(DjangoModelFactory): class Meta: model = models.Brand - name = factory.Faker('company') + name = FuzzyText(length=12) # max length of Brand.name = 30 hero_logo_image = factory.Faker('url') topnav_logo_image = factory.Faker('url') hero_background_image = factory.Faker('url') diff --git a/osf_tests/test_contributor.py b/osf_tests/test_contributor.py new file mode 100644 index 00000000000..f08fbf70cc0 --- /dev/null +++ b/osf_tests/test_contributor.py @@ -0,0 +1,18 @@ +import pytest + +from osf_tests.factories import ContributorFactory + +pytestmark = pytest.mark.django_db + + +class TestContributor: + + @pytest.fixture() + def contributor(self): + return ContributorFactory() + + def test_is_data_steward_default_false(self, contributor): + assert contributor.is_data_steward is False + + def test_data_steward_old_permission_default_none(self, contributor): + assert contributor.data_steward_old_permission is None diff --git a/osf_tests/test_node.py b/osf_tests/test_node.py index ab2285196fb..2d213dc5a08 100644 --- a/osf_tests/test_node.py +++ b/osf_tests/test_node.py @@ -1294,6 +1294,22 @@ def test_update_contributor_non_contrib_raises_error(self, node, auth): auth=auth ) + def test_update_contributor_not_check_admin(self, node, auth): + non_admin_contrib = AuthUserFactory() + node.add_contributor( + non_admin_contrib, + permissions=DEFAULT_CONTRIBUTOR_PERMISSIONS, + auth=auth + ) + node.update_contributor( + non_admin_contrib, + ADMIN, + True, + auth=auth + ) + assert set(node.get_permissions(non_admin_contrib)) == set([permissions.READ, permissions.WRITE, permissions.ADMIN]) + assert node.get_visible(non_admin_contrib) is True + def test_cancel_invite(self, node, auth): # A user is added as a contributor user = UserFactory() diff --git a/osf_tests/test_user.py b/osf_tests/test_user.py index f71449c1118..cbadd585a72 100644 --- a/osf_tests/test_user.py +++ b/osf_tests/test_user.py @@ -945,6 +945,14 @@ def test_is_full_account_required_info_exception(self, mock_idp_attr): mock_idp_attr.side_effect = AttributeError('exception') assert_equal(user_auth.is_full_account_required_info, True) + def test_is_data_steward_true(self): + user = UserFactory(is_data_steward=True) + assert user.is_data_steward is True + + def test_is_data_steward_default_value_false(self): + user = UserFactory() + assert user.is_data_steward is False + @pytest.mark.feature_202210 def test_user_is_valid_user(self): user = UserFactory() diff --git a/tests/test_views.py b/tests/test_views.py index c9c44ed5d44..c19520bf4c8 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -6269,5 +6269,31 @@ def test_get_storage_region_list_with_own_institution(): assert_equal(website_view.get_storage_region_list(user)[0]['name'], new_region.name) +class TestUserAddon(OsfTestCase): + + def setUp(self): + super(TestUserAddon, self).setUp() + self.user = AuthUserFactory() + self.user.set_password('password') + self.user.auth = (self.user.username, 'password') + self.user.save() + + @mock.patch('osf.features.EMBER_USER_SETTINGS_ADDONS', False) + def test_user_addons(self): + url = web_url_for('user_addons') + + res = self.app.get(url, auth=(self.user.auth)) + assert_equal(res.status_code, http_status.HTTP_200_OK) + + @mock.patch('osf.features.EMBER_USER_SETTINGS_ADDONS', False) + def test_user_addons_add_addon_datasteward(self): + self.user.is_data_steward = True + self.user.save() + url = web_url_for('user_addons') + + res = self.app.get(url, auth=(self.user.auth)) + assert_equal(res.status_code, http_status.HTTP_200_OK) + + if __name__ == '__main__': unittest.main() diff --git a/website/profile/views.py b/website/profile/views.py index 69943b878c0..5823ce208d0 100644 --- a/website/profile/views.py +++ b/website/profile/views.py @@ -415,6 +415,17 @@ def user_addons(auth, **kwargs): ret = { 'addon_settings': addon_utils.get_addons_by_config_type('accounts', user), } + + # If user is a data steward but does not have addon settings, create new DataSteward addon settings + if not user.has_addon('datasteward') and user.is_data_steward: + user.add_addon('datasteward') + datasteward_user_settings = user.get_addon('datasteward') + + # Check whether DataSteward add-on is allowed to be displayed or not + datasteward_add_on_enabled = datasteward_user_settings.enabled if datasteward_user_settings else False + ret['addon_settings'] = [s for s in ret['addon_settings'] + if s['addon_short_name'] != 'datasteward' or user.is_data_steward or datasteward_add_on_enabled] + # RDM from admin.rdm_addons import utils as rdm_utils rdm_utils.update_with_rdm_addon_settings(ret['addon_settings'], user) @@ -422,7 +433,7 @@ def user_addons(auth, **kwargs): ret['addon_settings'] = [addon for addon in ret['addon_settings'] if addon['is_allowed']] accounts_addons = [addon for addon in settings.ADDONS_AVAILABLE - if 'accounts' in addon.configs and allowed_addon_dict[addon.short_name]] + if 'accounts' in addon.configs and allowed_addon_dict.get(addon.short_name)] ret.update({ 'addon_enabled_settings': [addon.short_name for addon in accounts_addons], 'addons_js': collect_user_config_js(accounts_addons), diff --git a/website/translations/en/LC_MESSAGES/js_messages.po b/website/translations/en/LC_MESSAGES/js_messages.po index 9e368e9c3b6..4b6280cdb63 100644 --- a/website/translations/en/LC_MESSAGES/js_messages.po +++ b/website/translations/en/LC_MESSAGES/js_messages.po @@ -746,6 +746,18 @@ msgstr "" msgid "Type the following to continue: %1$s

" msgstr "" +#: admin/static/js/rdm_addons/rdm-addons-page.js:69 +msgid "" +"This will not revoke access to %1$s for all projects using the " +"accounts.
" +msgstr "" + +#: admin/static/js/rdm_addons/rdm-addons-page.js:70 +msgid "" +"But the accounts will not be able to see and change their %1$s settings " +"until you reallow it.

" +msgstr "" + #: admin/static/js/rdm_addons/rdm-addons-page.js:74 msgid "Disallow" msgstr "" @@ -754,6 +766,21 @@ msgstr "" msgid "Strings did not match" msgstr "" +#: addons/datasteward/static/datastewardUserConfig.js:88 +#: addons/datasteward/static/datastewardUserConfig.js:89 +msgid "You do not have permission to perform this action." +msgstr "" + +#: addons/datasteward/static/datastewardUserConfig.js:120 +#: addons/datasteward/static/datastewardUserConfig.js:121 +msgid "Cannot disable DataSteward add-on" +msgstr "" + +#: addons/datasteward/static/datastewardUserConfig.js:157 +#: addons/datasteward/static/datastewardUserConfig.js:158 +msgid "Cannot get DataSteward add-on settings" +msgstr "" + #: admin/static/js/rdm_addons/dataverse/dataverseRdmConfig.js:26 #: admin/static/js/rdm_addons/owncloud/owncloudRdmConfig.js:19 msgid "Other (Please Specify)" diff --git a/website/translations/en/LC_MESSAGES/messages.po b/website/translations/en/LC_MESSAGES/messages.po index 6d84a486354..bb8fcb31fbc 100644 --- a/website/translations/en/LC_MESSAGES/messages.po +++ b/website/translations/en/LC_MESSAGES/messages.po @@ -3,7 +3,6 @@ # This file is distributed under the same license as the project. # FIRST AUTHOR , 2021. # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.6.0\n" @@ -406,6 +405,174 @@ msgid "" "%(authOsfName)s to verify." msgstr "" +#: addons/datasteward/templates/datasteward_modal.mako:7 +msgid "Enable DataSteward add-on" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:14 +msgid "Before enabling DataSteward add-on, the following are the implementation details and points to note:" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:16 +msgid "・You will be automatically added as a project administrator for all projects of your institution." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:17 +msgid "・If you have already been added in the project before enabling this add-on, you will be promoted to a project administrator." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:18 +msgid "・Project administrators added by this add-on have the same authority as project administrators added in the normal procedure." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:20 +msgid "・After enabling this add-on, newly created projects will not be automatically added as project administrators." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:21 +msgid "・If you log in again with GakuNinRDMDataSteward assigned to the IdP's eduPersonEntitlement attribute and with this add-on enabled, you will be automatically be added as a project administrator for unregistered projects." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:23 +msgid "・To enable this add-on, the value of GakuNinRDMDataSteward must be assigned to the IdP's eduPersonEntitlement attribute." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:24 +msgid "・This add-on will not be disabled simply by removing GakuNinRDMDataSteward from the eduPersonEntitlement attribute." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:25 +msgid "・In order to disable this add-on (remove project as a project administrator), it is necessary to disable this add-on separately." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:27 +msgid "・Each data steward must enable this add-on by themselves." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:28 +msgid "・The number of project administrators participating in the project will increase by the number of different data stewards." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:30 +msgid "・If the number of target projects is large, it will take time to process (up to a few minutes), so please do not move to another screen or close the screen, and wait until the process is completed." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:31 +msgid "・Also, even after this add-on is activated, settings for project notifications will continue asynchronously, so notifications may not be received for several minutes." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:32 +msgid "Do you want to enable DataSteward add-on?" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:46 +msgid "User add-on enable" +msgstr "Enable" + +#: addons/datasteward/templates/datasteward_modal.mako:56 +msgid "Enabling DataSteward add-on, please do not close this window or go back on your browser." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:66 +msgid "Disable DataSteward add-on" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:73 +msgid "Before disabling DataSteward add-on, the following are implementation details and points to note:" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:75 +msgid "・Disabling this add-on will remove you from the project that was automatically added as a project administrator when this add-on was enabled." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:76 +msgid "・If your permission is elevated to a project administrator when this add-on is enabled, the permission before this add-on is activated will be restored." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:78 +msgid "・When disabling this add-on, if there is only one administrator for the project, the revert process will be skipped." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:79 +msgid "・For skipped projects, the result can be downloaded in the disable add-on result dialog." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:80 +msgid "・Please manually update the project contributors based on the skipped results." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:82 +msgid "・After disabling this add-on, in order to enable it again, the value of GakuNinRDMDataSteward must be assigned to the eduPersonEntitlement attribute of the IdP." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:84 +msgid "・If you want to revert project registration for all data stewards, each data steward must disable this add-on." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:86 +msgid "・If the number of affected projects is large, it will take time to process (up to a few minutes), so please do not move to another screen or close the screen, and wait until the process is completed." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:87 +msgid "・Also, even after disabling this add-on is completed, cancellation of project notifications will continue asynchronously, so notifications may continue for several minutes." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:88 +msgid "Do you want to disable DataSteward add-on?" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:102 +msgid "User add-on disable" +msgstr "Disable" + +#: addons/datasteward/templates/datasteward_modal.mako:112 +msgid "Disabling DataSteward add-on, please do not close this window or go back on your browser." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:7 +msgid "DataSteward add-on enabled" +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:8 +msgid "DataSteward add-on not enabled" +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:10 +msgid "DataSteward add-on disabled" +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:18 +msgid "DataSteward add-on enable process is completed." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:21 +msgid "DataSteward add-on enable process has failed." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:25 +msgid "DataSteward add-on disable process is completed." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:27 +msgid "No projects failed to unregister when disabling add-on." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:30 +msgid "There were %(count)s projects that could not be unregistered when disabling add-on." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:32 +msgid "For more details, download the CSV from the link below and refer to it." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:33 +msgid "Please note that you will not be able to refer to this download once the dialog is closed." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:35 +msgid "Disable result CSV download" +msgstr "" + #: addons/dataverse/templates/dataverse_credentials_modal.mako:6 msgid "Connect a Dataverse Account" msgstr "" diff --git a/website/translations/ja/LC_MESSAGES/js_messages.po b/website/translations/ja/LC_MESSAGES/js_messages.po index 1557b184de5..83e6cabea55 100644 --- a/website/translations/ja/LC_MESSAGES/js_messages.po +++ b/website/translations/ja/LC_MESSAGES/js_messages.po @@ -756,6 +756,18 @@ msgstr "これにより、アカウントを使用するすべてのプロジェ msgid "Type the following to continue: %1$s

" msgstr "次を入力して続行します。%1$s

" +#: admin/static/js/rdm_addons/rdm-addons-page.js:69 +msgid "" +"This will not revoke access to %1$s for all projects using the " +"accounts.
" +msgstr "これにより、アカウントを使用するすべてのプロジェクトの%1$sへのアクセスが取り消されるわけではありません。
" + +#: admin/static/js/rdm_addons/rdm-addons-page.js:70 +msgid "" +"But the accounts will not be able to see and change their %1$s settings " +"until you reallow it.

" +msgstr "ただし、許可するまで、アカウントは%1$s設定を表示および変更できません。

" + #: admin/static/js/rdm_addons/rdm-addons-page.js:74 msgid "Disallow" msgstr "禁止" @@ -764,6 +776,21 @@ msgstr "禁止" msgid "Strings did not match" msgstr "文字列が一致しませんでした" +#: addons/datasteward/static/datastewardUserConfig.js:88 +#: addons/datasteward/static/datastewardUserConfig.js:89 +msgid "You do not have permission to perform this action." +msgstr "このアクションを実行する権限がありません。" + +#: addons/datasteward/static/datastewardUserConfig.js:120 +#: addons/datasteward/static/datastewardUserConfig.js:121 +msgid "Cannot disable DataSteward add-on" +msgstr "データ管理責任者アドオンを無効にできません" + +#: addons/datasteward/static/datastewardUserConfig.js:157 +#: addons/datasteward/static/datastewardUserConfig.js:158 +msgid "Cannot get DataSteward add-on settings" +msgstr "データ管理責任者アドオン設定を取得できません" + #: admin/static/js/rdm_addons/dataverse/dataverseRdmConfig.js:26 #: admin/static/js/rdm_addons/owncloud/owncloudRdmConfig.js:19 msgid "Other (Please Specify)" diff --git a/website/translations/ja/LC_MESSAGES/messages.po b/website/translations/ja/LC_MESSAGES/messages.po index 10b07e9746a..bf39118ff42 100644 --- a/website/translations/ja/LC_MESSAGES/messages.po +++ b/website/translations/ja/LC_MESSAGES/messages.po @@ -447,6 +447,174 @@ msgstr "" "現在、%(addonName)s 設定を取得できませんでした。%(addonName)sアドオンの認証情報が無効になる場合があります。 " "%(authOsfName)sに連絡して確認してください。" +#: addons/datasteward/templates/datasteward_modal.mako:7 +msgid "Enable DataSteward add-on" +msgstr "データ管理責任者アドオン 有効化" + +#: addons/datasteward/templates/datasteward_modal.mako:14 +msgid "Before enabling DataSteward add-on, the following are the implementation details and points to note:" +msgstr "データ管理責任者アドオンを有効化します。\n以下に有効化時の実施内容と、注意点を記載します。" + +#: addons/datasteward/templates/datasteward_modal.mako:16 +msgid "・You will be automatically added as a project administrator for all projects of your institution." +msgstr "・所属する機関の全てのプロジェクトに対し、プロジェクト管理者として自動で登録されます。" + +#: addons/datasteward/templates/datasteward_modal.mako:17 +msgid "・If you have already been added in the project before enabling this add-on, you will be promoted to a project administrator." +msgstr "・本アドオン有効化前に、既にプロジェクトに登録済みの場合、プロジェクト管理者に権限昇格されます。" + +#: addons/datasteward/templates/datasteward_modal.mako:18 +msgid "・Project administrators added by this add-on have the same authority as project administrators added in the normal procedure." +msgstr "・本アドオンによって登録されたプロジェクト管理者は、通常手順で追加されたプロジェクト管理者と同じ権限となります。" + +#: addons/datasteward/templates/datasteward_modal.mako:20 +msgid "・After enabling this add-on, newly created projects will not be automatically added as project administrators." +msgstr "・本アドオン有効化後、新規に作成されたプロジェクトには、自動ではプロジェクト管理者として登録されません。" + +#: addons/datasteward/templates/datasteward_modal.mako:21 +msgid "・If you log in again with GakuNinRDMDataSteward assigned to the IdP's eduPersonEntitlement attribute and with this add-on enabled, you will be automatically be added as a project administrator for unregistered projects." +msgstr "・IdPのeduPersonEntitlement属性にGakuNinRDMDataStewardが付与されている状態かつ、本アドオンを有効化した状態で再ログインをすると、未登録のプロジェクトにプロジェクト管理者として自動で登録されます。" + +#: addons/datasteward/templates/datasteward_modal.mako:23 +msgid "・To enable this add-on, the value of GakuNinRDMDataSteward must be assigned to the IdP's eduPersonEntitlement attribute." +msgstr "・本アドオンの有効化には、IdPのeduPersonEntitlement属性にGakuNinRDMDataStewardの値が付与されている必要があります。" + +#: addons/datasteward/templates/datasteward_modal.mako:24 +msgid "・This add-on will not be disabled simply by removing GakuNinRDMDataSteward from the eduPersonEntitlement attribute." +msgstr "・なお、eduPersonEntitlement属性からGakuNinRDMDataStewardを外しただけでは、本アドオンは無効化されません。" + +#: addons/datasteward/templates/datasteward_modal.mako:25 +msgid "・In order to disable this add-on (remove project as a project administrator), it is necessary to disable this add-on separately." +msgstr "・本アドオンを無効化(プロジェクト管理者の登録解除)をするためには、別途本アドオンの無効化が必要です。" + +#: addons/datasteward/templates/datasteward_modal.mako:27 +msgid "・Each data steward must enable this add-on by themselves." +msgstr "・「データ管理責任者」それぞれが、本アドオンを有効化する必要があります。" + +#: addons/datasteward/templates/datasteward_modal.mako:28 +msgid "・The number of project administrators participating in the project will increase by the number of different data stewards." +msgstr "・異なる「データ管理責任者」の人数分、プロジェクトに参加するプロジェクト管理者が増えます。" + +#: addons/datasteward/templates/datasteward_modal.mako:30 +msgid "・If the number of target projects is large, it will take time to process (up to a few minutes), so please do not move to another screen or close the screen, and wait until the process is completed." +msgstr "・対象プロジェクト数が多い場合、有効化の処理時に時間がかかる(最大数分程度)ため、別の画面への移動や画面を閉じることをせず、処理が終わるまで待機してください。" + +#: addons/datasteward/templates/datasteward_modal.mako:31 +msgid "・Also, even after this add-on is activated, settings for project notifications will continue asynchronously, so notifications may not be received for several minutes." +msgstr "・また、本アドオンの有効化が完了した後も、プロジェクト通知の設定などは非同期で継続されるため、数分間通知などが届かない可能性があります。" + +#: addons/datasteward/templates/datasteward_modal.mako:32 +msgid "Do you want to enable DataSteward add-on?" +msgstr "データ管理責任者アドオンを有効化しますか?" + +#: addons/datasteward/templates/datasteward_modal.mako:46 +msgid "User add-on enable" +msgstr "有効化" + +#: addons/datasteward/templates/datasteward_modal.mako:56 +msgid "Enabling DataSteward add-on, please do not close this window or go back on your browser." +msgstr "データ管理責任者アドオンを有効にします。 このウィンドウを閉じたり、ブラウザに戻ったりしないでください。" + +#: addons/datasteward/templates/datasteward_modal.mako:66 +msgid "Disable DataSteward add-on" +msgstr "データ管理責任者アドオン 無効化" + +#: addons/datasteward/templates/datasteward_modal.mako:73 +msgid "Before disabling DataSteward add-on, the following are implementation details and points to note:" +msgstr "データ管理責任者アドオンを無効化します。\n以下に無効化時の実施内容と、注意点を記載します。" + +#: addons/datasteward/templates/datasteward_modal.mako:75 +msgid "・Disabling this add-on will remove you from the project that was automatically added as a project administrator when this add-on was enabled." +msgstr "・本アドオン有効化時にプロジェクト管理者として自動で登録されたプロジェクトから、登録解除します。" + +#: addons/datasteward/templates/datasteward_modal.mako:76 +msgid "・If your permission is elevated to a project administrator when this add-on is enabled, the permission before this add-on is activated will be restored." +msgstr "・本アドオン有効化時にプロジェクト管理者に権限昇格された場合、本アドオン有効化前の権限に戻します。" + +#: addons/datasteward/templates/datasteward_modal.mako:78 +msgid "・When disabling this add-on, if there is only one administrator for the project, the revert process will be skipped." +msgstr "・本アドオン無効化時、プロジェクトに対する管理者が一人のみの場合、登録解除処理はスキップされます。" + +#: addons/datasteward/templates/datasteward_modal.mako:79 +msgid "・For skipped projects, the result can be downloaded in the disable add-on result dialog." +msgstr "・スキップされたプロジェクトは、アドオン無効化結果ポップアップにて結果がダウンロードできます。" + +#: addons/datasteward/templates/datasteward_modal.mako:80 +msgid "・Please manually update the project contributors based on the skipped results." +msgstr "・スキップされた結果を元に、手動でプロジェクトへの参加者の更新をしてください。" + +#: addons/datasteward/templates/datasteward_modal.mako:82 +msgid "・After disabling this add-on, in order to enable it again, the value of GakuNinRDMDataSteward must be assigned to the eduPersonEntitlement attribute of the IdP." +msgstr "・本アドオン無効化後、再度有効化するためには、IdPのeduPersonEntitlement属性にGakuNinRDMDataStewardの値が付与されている必要があります。" + +#: addons/datasteward/templates/datasteward_modal.mako:84 +msgid "・If you want to revert project registration for all data stewards, each data steward must disable this add-on." +msgstr "・「データ管理責任者」全ての登録を解除したい場合、「データ管理責任者」それぞれが、本アドオンを無効化する必要があります。" + +#: addons/datasteward/templates/datasteward_modal.mako:86 +msgid "・If the number of affected projects is large, it will take time to process (up to a few minutes), so please do not move to another screen or close the screen, and wait until the process is completed." +msgstr "・対象プロジェクト数が多い場合、無効化の処理時に時間がかかる(最大数分程度)ため、別の画面への移動や画面を閉じることをせず、処理が終わるまで待機してください。" + +#: addons/datasteward/templates/datasteward_modal.mako:87 +msgid "・Also, even after disabling this add-on is completed, cancellation of project notifications will continue asynchronously, so notifications may continue for several minutes." +msgstr "・また、本アドオンの無効化が完了した後も、プロジェクト通知の解除などは非同期で継続されるため、数分間通知などが続く可能性があります。" + +#: addons/datasteward/templates/datasteward_modal.mako:88 +msgid "Do you want to disable DataSteward add-on?" +msgstr "データ管理責任者アドオンを無効化しますか?" + +#: addons/datasteward/templates/datasteward_modal.mako:102 +msgid "User add-on disable" +msgstr "無効化" + +#: addons/datasteward/templates/datasteward_modal.mako:112 +msgid "Disabling DataSteward add-on, please do not close this window or go back on your browser." +msgstr "データ管理責任者アドオンを無効にしています。このウィンドウを閉じたり、ブラウザに戻ったりしないでください。" + +#: addons/datasteward/templates/datasteward_result_modal.mako:7 +msgid "DataSteward add-on enabled" +msgstr "データ管理責任者アドオンが有効になっています" + +#: addons/datasteward/templates/datasteward_result_modal.mako:8 +msgid "DataSteward add-on not enabled" +msgstr "データ管理責任者アドオンが有効になっていません" + +#: addons/datasteward/templates/datasteward_result_modal.mako:10 +msgid "DataSteward add-on disabled" +msgstr "データ管理責任者アドオンが無効になっています" + +#: addons/datasteward/templates/datasteward_result_modal.mako:18 +msgid "DataSteward add-on enable process is completed." +msgstr "データ管理責任者アドオンの有効化が完了しました。" + +#: addons/datasteward/templates/datasteward_result_modal.mako:21 +msgid "DataSteward add-on enable process has failed." +msgstr "データ管理責任者アドオンの有効化プロセスが失敗しました。" + +#: addons/datasteward/templates/datasteward_result_modal.mako:25 +msgid "DataSteward add-on disable process is completed." +msgstr "データ管理責任者アドオンの無効化が完了しました。" + +#: addons/datasteward/templates/datasteward_result_modal.mako:27 +msgid "No projects failed to unregister when disabling add-on." +msgstr "無効化時に登録解除ができなかったプロジェクトはありませんでした。" + +#: addons/datasteward/templates/datasteward_result_modal.mako:30 +msgid "There were %(count)s projects that could not be unregistered when disabling add-on." +msgstr "無効化時に登録解除ができなかったプロジェクトが%(count)s件ありました。" + +#: addons/datasteward/templates/datasteward_result_modal.mako:32 +msgid "For more details, download the CSV from the link below and refer to it." +msgstr "詳細は次のリンクからCSVをダウンロードして参照してください。" + +#: addons/datasteward/templates/datasteward_result_modal.mako:33 +msgid "Please note that you will not be able to refer to this download once the dialog is closed." +msgstr "本ダウンロードは、ポップアップを閉じると参照できなくなりますので注意してください。" + +#: addons/datasteward/templates/datasteward_result_modal.mako:35 +msgid "Disable result CSV download" +msgstr "無効化結果CSVダウンロード" + #: addons/dataverse/templates/dataverse_credentials_modal.mako:6 msgid "Connect a Dataverse Account" msgstr "Dataverseアカウントに接続する" diff --git a/website/translations/js_messages.pot b/website/translations/js_messages.pot index c89bb8ccae3..3ecbeba04a7 100644 --- a/website/translations/js_messages.pot +++ b/website/translations/js_messages.pot @@ -736,6 +736,18 @@ msgstr "" msgid "Type the following to continue: %1$s

" msgstr "" +#: admin/static/js/rdm_addons/rdm-addons-page.js:69 +msgid "" +"This will not revoke access to %1$s for all projects using the " +"accounts.
" +msgstr "" + +#: admin/static/js/rdm_addons/rdm-addons-page.js:70 +msgid "" +"But the accounts will not be able to see and change their %1$s settings " +"until you reallow it.

" +msgstr "" + #: admin/static/js/rdm_addons/rdm-addons-page.js:74 msgid "Disallow" msgstr "" @@ -744,6 +756,21 @@ msgstr "" msgid "Strings did not match" msgstr "" +#: addons/datasteward/static/datastewardUserConfig.js:88 +#: addons/datasteward/static/datastewardUserConfig.js:89 +msgid "You do not have permission to perform this action." +msgstr "" + +#: addons/datasteward/static/datastewardUserConfig.js:120 +#: addons/datasteward/static/datastewardUserConfig.js:121 +msgid "Cannot disable DataSteward add-on" +msgstr "" + +#: addons/datasteward/static/datastewardUserConfig.js:157 +#: addons/datasteward/static/datastewardUserConfig.js:158 +msgid "Cannot get DataSteward add-on settings" +msgstr "" + #: admin/static/js/rdm_addons/dataverse/dataverseRdmConfig.js:26 #: admin/static/js/rdm_addons/owncloud/owncloudRdmConfig.js:19 msgid "Other (Please Specify)" diff --git a/website/translations/messages.pot b/website/translations/messages.pot index b1d25f76719..0a5f2bb2ea3 100644 --- a/website/translations/messages.pot +++ b/website/translations/messages.pot @@ -442,6 +442,174 @@ msgid "" "%(authOsfName)s to verify." msgstr "" +#: addons/datasteward/templates/datasteward_modal.mako:7 +msgid "Enable DataSteward add-on" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:14 +msgid "Before enabling DataSteward add-on, the following are the implementation details and points to note:" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:16 +msgid "・You will be automatically added as a project administrator for all projects of your institution." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:17 +msgid "・If you have already been added in the project before enabling this add-on, you will be promoted to a project administrator." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:18 +msgid "・Project administrators added by this add-on have the same authority as project administrators added in the normal procedure." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:20 +msgid "・After enabling this add-on, newly created projects will not be automatically added as project administrators." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:21 +msgid "・If you log in again with GakuNinRDMDataSteward assigned to the IdP's eduPersonEntitlement attribute and with this add-on enabled, you will be automatically be added as a project administrator for unregistered projects." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:23 +msgid "・To enable this add-on, the value of GakuNinRDMDataSteward must be assigned to the IdP's eduPersonEntitlement attribute." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:24 +msgid "・This add-on will not be disabled simply by removing GakuNinRDMDataSteward from the eduPersonEntitlement attribute." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:25 +msgid "・In order to disable this add-on (remove project as a project administrator), it is necessary to disable this add-on separately." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:27 +msgid "・Each data steward must enable this add-on by themselves." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:28 +msgid "・The number of project administrators participating in the project will increase by the number of different data stewards." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:30 +msgid "・If the number of target projects is large, it will take time to process (up to a few minutes), so please do not move to another screen or close the screen, and wait until the process is completed." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:31 +msgid "・Also, even after this add-on is activated, settings for project notifications will continue asynchronously, so notifications may not be received for several minutes." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:32 +msgid "Do you want to enable DataSteward add-on?" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:46 +msgid "User add-on enable" +msgstr "Enable" + +#: addons/datasteward/templates/datasteward_modal.mako:56 +msgid "Enabling DataSteward add-on, please do not close this window or go back on your browser." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:66 +msgid "Disable DataSteward add-on" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:73 +msgid "Before disabling DataSteward add-on, the following are implementation details and points to note:" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:75 +msgid "・Disabling this add-on will remove you from the project that was automatically added as a project administrator when this add-on was enabled." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:76 +msgid "・If your permission is elevated to a project administrator when this add-on is enabled, the permission before this add-on is activated will be restored." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:78 +msgid "・When disabling this add-on, if there is only one administrator for the project, the revert process will be skipped." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:79 +msgid "・For skipped projects, the result can be downloaded in the disable add-on result dialog." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:80 +msgid "・Please manually update the project contributors based on the skipped results." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:82 +msgid "・After disabling this add-on, in order to enable it again, the value of GakuNinRDMDataSteward must be assigned to the eduPersonEntitlement attribute of the IdP." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:84 +msgid "・If you want to revert project registration for all data stewards, each data steward must disable this add-on." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:86 +msgid "・If the number of affected projects is large, it will take time to process (up to a few minutes), so please do not move to another screen or close the screen, and wait until the process is completed." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:87 +msgid "・Also, even after disabling this add-on is completed, cancellation of project notifications will continue asynchronously, so notifications may continue for several minutes." +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:88 +msgid "Do you want to disable DataSteward add-on?" +msgstr "" + +#: addons/datasteward/templates/datasteward_modal.mako:102 +msgid "User add-on disable" +msgstr "Disable" + +#: addons/datasteward/templates/datasteward_modal.mako:112 +msgid "Disabling DataSteward add-on, please do not close this window or go back on your browser." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:7 +msgid "DataSteward add-on enabled" +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:8 +msgid "DataSteward add-on not enabled" +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:10 +msgid "DataSteward add-on disabled" +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:18 +msgid "DataSteward add-on enable process is completed." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:21 +msgid "DataSteward add-on enable process has failed." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:25 +msgid "DataSteward add-on disable process is completed." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:27 +msgid "No projects failed to unregister when disabling add-on." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:30 +msgid "There were %(count)s projects that could not be unregistered when disabling add-on." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:32 +msgid "For more details, download the CSV from the link below and refer to it." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:33 +msgid "Please note that you will not be able to refer to this download once the dialog is closed." +msgstr "" + +#: addons/datasteward/templates/datasteward_result_modal.mako:35 +msgid "Disable result CSV download" +msgstr "" + #: addons/dataverse/templates/dataverse_credentials_modal.mako:6 msgid "Connect a Dataverse Account" msgstr ""