diff --git a/doc/conf.py b/doc/conf.py index 19e09cddbc..09c5285411 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -73,9 +73,9 @@ # # The short X.Y version. -version = 'v2.7.1' +version = 'v2.7.2' # The full version, including alpha/beta/rc tags. -release = 'v2.7.1' +release = 'v2.7.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/user/project_settings.rst b/doc/user/project_settings.rst index 6975a256ee..642b01988a 100644 --- a/doc/user/project_settings.rst +++ b/doc/user/project_settings.rst @@ -314,3 +314,18 @@ and task runs, use this section to delete the project. .. note:: Only projects without results can be deleted. + +Transfer project ownership +========================== + +You can transfer the project ownership to another user in the PYBOSSA server. + +For changing the ownership, just add the email of the user that you want to make +the new owner. + +.. note:: + If your are not an admin of PYBOSSA you cannot undo this action, and you will + not be able to modify/change settings of the project as you will not be the + owner anymore. Admins however can change the ownership always. + + diff --git a/pybossa/forms/forms.py b/pybossa/forms/forms.py index 7de758d024..b7c2a72298 100644 --- a/pybossa/forms/forms.py +++ b/pybossa/forms/forms.py @@ -475,3 +475,6 @@ class AvatarUploadForm(Form): y1 = IntegerField(label=None, widget=HiddenInput(), default=0) x2 = IntegerField(label=None, widget=HiddenInput(), default=0) y2 = IntegerField(label=None, widget=HiddenInput(), default=0) + +class TransferOwnershipForm(Form): + email_addr = EmailField(lazy_gettext('Email of the new owner')) diff --git a/pybossa/forms/projects_view_forms.py b/pybossa/forms/projects_view_forms.py index fa0620a4e6..90e9a55aad 100644 --- a/pybossa/forms/projects_view_forms.py +++ b/pybossa/forms/projects_view_forms.py @@ -27,4 +27,5 @@ BlogpostForm, PasswordForm, GenericBulkTaskImportForm, - AvatarUploadForm) + AvatarUploadForm, + TransferOwnershipForm) diff --git a/pybossa/themes/default b/pybossa/themes/default index 082fda77c7..b0f7b74e95 160000 --- a/pybossa/themes/default +++ b/pybossa/themes/default @@ -1 +1 @@ -Subproject commit 082fda77c762eaf6fa423bc76d717e543110fa05 +Subproject commit b0f7b74e95c21972f926f3c263ee6d676e4813e4 diff --git a/pybossa/view/projects.py b/pybossa/view/projects.py index 0184ad55e1..00e65144b9 100644 --- a/pybossa/view/projects.py +++ b/pybossa/view/projects.py @@ -1822,3 +1822,50 @@ def reset_secret_key(short_name): msg = gettext('New secret key generated') flash(msg, 'success') return redirect_content_type(url_for('.update', short_name=short_name)) + +@blueprint.route('//transferownership', methods=['GET', 'POST']) +@login_required +def transfer_ownership(short_name): + """Transfer project ownership.""" + + project, owner, ps = project_by_shortname(short_name) + + pro = pro_features() + + title = project_title(project, "Results") + + ensure_authorized_to('update', project) + + form = TransferOwnershipForm(request.body) + + if request.method == 'POST' and form.validate(): + new_owner = user_repo.filter_by(email_addr=form.email_addr.data) + if len(new_owner) == 1: + new_owner = new_owner[0] + project.owner_id = new_owner.id + project_repo.update(project) + msg = gettext("Project owner updated") + return redirect_content_type(url_for('.details', + short_name=short_name)) + else: + msg = gettext("New project owner not found by email") + flash(msg, 'info') + return redirect_content_type(url_for('.transfer_ownership', + short_name=short_name)) + else: + owner_serialized = cached_users.get_user_summary(owner.name) + project = add_custom_contrib_button_to(project, get_user_id_or_ip(), ps=ps) + response = dict(template='/projects/transferownership.html', + project=project, + owner=owner_serialized, + n_tasks=ps.n_tasks, + overall_progress=ps.overall_progress, + n_task_runs=ps.n_task_runs, + last_activity=ps.last_activity, + n_completed_tasks=ps.n_completed_tasks, + n_volunteers=ps.n_volunteers, + title=title, + pro_features=pro, + form=form, + target='.transfer_ownership') + return handle_content_type(response) diff --git a/setup.py b/setup.py index 9aa2ba7334..9e707f0e28 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name = 'pybossa', - version = '2.7.1', + version = '2.7.2', packages = find_packages(), install_requires = requirements, # only needed when installing directly from setup.py (PyPi, eggs?) and pointing to e.g. a git repo. diff --git a/test/test_view/test_project_transferownership.py b/test/test_view/test_project_transferownership.py new file mode 100644 index 0000000000..810ff12c05 --- /dev/null +++ b/test/test_view/test_project_transferownership.py @@ -0,0 +1,143 @@ +# -*- coding: utf8 -*- +# This file is part of PYBOSSA. +# +# Copyright (C) 2017 Scifabric +# +# PYBOSSA is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PYBOSSA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with PYBOSSA. If not, see . +from default import db, with_context +from factories import ProjectFactory, UserFactory +from helper import web +from pybossa.repositories import UserRepository, ProjectRepository +import json + +project_repo = ProjectRepository(db) +user_repo = UserRepository(db) + +class TestProjectTransferOwnership(web.Helper): + + @with_context + def test_transfer_anon_get(self): + """Test transfer ownership page is not shown to anon.""" + project = ProjectFactory.create() + url = '/project/%s/transferownership' % project.short_name + res = self.app_get_json(url, follow_redirects=True) + assert 'signin' in res.data, res.data + + @with_context + def test_transfer_auth_not_owner_get(self): + """Test transfer ownership page is forbidden for not owner.""" + admin, owner, user = UserFactory.create_batch(3) + project = ProjectFactory.create(owner=owner) + url = '/project/%s/transferownership?api_key=%s' % (project.short_name, + user.api_key) + res = self.app_get_json(url, follow_redirects=True) + data = json.loads(res.data) + assert data['code'] == 403, data + + @with_context + def test_transfer_auth_owner_get(self): + """Test transfer ownership page is ok for owner.""" + admin, owner, user = UserFactory.create_batch(3) + project = ProjectFactory.create(owner=owner) + url = '/project/%s/transferownership?api_key=%s' % (project.short_name, + owner.api_key) + res = self.app_get_json(url, follow_redirects=True) + data = json.loads(res.data) + assert data['form'], data + assert data['form']['errors'] == {}, data + assert data['form']['email_addr'] is None, data + assert data['form']['csrf'] is not None, data + + @with_context + def test_transfer_auth_admin_get(self): + """Test transfer ownership page is ok for admin.""" + admin, owner, user = UserFactory.create_batch(3) + project = ProjectFactory.create(owner=owner) + url = '/project/%s/transferownership?api_key=%s' % (project.short_name, + admin.api_key) + res = self.app_get_json(url, follow_redirects=True) + data = json.loads(res.data) + assert data['form'], data + assert data['form']['errors'] == {}, data + assert data['form']['email_addr'] is None, data + assert data['form']['csrf'] is not None, data + + @with_context + def test_transfer_auth_owner_post(self): + """Test transfer ownership page post is ok for owner.""" + admin, owner, user = UserFactory.create_batch(3) + project = ProjectFactory.create(owner=owner) + url = '/project/%s/transferownership?api_key=%s' % (project.short_name, + owner.api_key) + + assert project.owner_id == owner.id + payload = dict(email_addr=user.email_addr) + res = self.app_post_json(url, data=payload, + follow_redirects=True) + data = json.loads(res.data) + assert data['next'] is not None, data + + err_msg = "The project owner id should be different" + assert project.owner_id == user.id, err_msg + + @with_context + def test_transfer_auth_owner_post_wrong_email(self): + """Test transfer ownership page post is ok for wrong email.""" + admin, owner, user = UserFactory.create_batch(3) + project = ProjectFactory.create(owner=owner) + url = '/project/%s/transferownership?api_key=%s' % (project.short_name, + owner.api_key) + + assert project.owner_id == owner.id + payload = dict(email_addr="wrong@email.com") + res = self.app_post_json(url, data=payload, + follow_redirects=True) + data = json.loads(res.data) + assert data['next'] is not None, data + assert "project owner not found" in data['flash'], data + err_msg = "The project owner id should be the same" + assert project.owner_id == owner.id, err_msg + + @with_context + def test_transfer_auth_admin_post(self): + """Test transfer ownership page post is ok for admin.""" + admin, owner, user = UserFactory.create_batch(3) + project = ProjectFactory.create(owner=owner) + url = '/project/%s/transferownership?api_key=%s' % (project.short_name, + admin.api_key) + + assert project.owner_id == owner.id + payload = dict(email_addr=user.email_addr) + res = self.app_post_json(url, data=payload, + follow_redirects=True) + data = json.loads(res.data) + assert data['next'] is not None, data + + err_msg = "The project owner id should be different" + assert project.owner_id == user.id, err_msg + + @with_context + def test_transfer_auth_user_post(self): + """Test transfer ownership page post is forbidden for not owner.""" + admin, owner, user = UserFactory.create_batch(3) + project = ProjectFactory.create(owner=owner) + url = '/project/%s/transferownership?api_key=%s' % (project.short_name, + user.api_key) + + assert project.owner_id == owner.id + payload = dict(email_addr=user.email_addr) + res = self.app_post_json(url, data=payload, + follow_redirects=True) + data = json.loads(res.data) + assert data['code'] == 403, data