Skip to content

Commit 6582cc1

Browse files
committed
Merge branch 'develop' of https://github.com/centerforopenscience/osf.io into feature/notification-refactor-p2-s
# Conflicts: # tests/test_adding_contributor_views.py
2 parents 7b09dc4 + 1dd3954 commit 6582cc1

File tree

11 files changed

+64
-50
lines changed

11 files changed

+64
-50
lines changed

admin/users/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ def get_claim_links(self, user):
512512

513513
for guid, value in user.unclaimed_records.items():
514514
obj = Guid.load(guid)
515-
url = f'{DOMAIN}user/{user._id}/{guid}/claim/?token={value["token"]}'
515+
url = f'{DOMAIN}legacy/user/{user._id}/{guid}/claim/?token={value["token"]}'
516516
links.append(f'Claim URL for {obj.content_type.model} {obj._id}: {url}')
517517

518518
return links or ['User currently has no active unclaimed records for any nodes.']

api/nodes/serializers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,12 @@ def update(self, instance, validated_data):
13081308
validated_data,
13091309
)
13101310

1311+
class NodeContributorsUpdateSerializer(ser.Serializer):
1312+
def update(self, instance, validated_data):
1313+
if project := instance.root:
1314+
instance.copy_contributors_from(project)
1315+
return instance
1316+
13111317

13121318
class NodeLinksSerializer(JSONAPISerializer):
13131319

api/nodes/views.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
DraftRegistrationDetailLegacySerializer,
110110
NodeContributorsSerializer,
111111
NodeContributorDetailSerializer,
112+
NodeContributorsUpdateSerializer,
112113
NodeInstitutionsRelationshipSerializer,
113114
NodeContributorsCreateSerializer,
114115
NodeViewOnlyLinkSerializer,
@@ -436,11 +437,16 @@ class NodeContributorsList(BaseContributorList, bulk_views.BulkUpdateJSONAPIView
436437
def get_resource(self):
437438
return self.get_node()
438439

440+
def get_object(self):
441+
return self.get_node()
442+
439443
# overrides ListBulkCreateJSONAPIView, BulkUpdateJSONAPIView, BulkDeleteJSONAPIView
440444
def get_serializer_class(self):
441445
"""
442446
Use NodeContributorDetailSerializer which requires 'id'
443447
"""
448+
if self.request.method == 'PATCH' and self.request.query_params.get('copy_contributors_from_parent_project'):
449+
return NodeContributorsUpdateSerializer
444450
if self.request.method == 'PUT' or self.request.method == 'PATCH' or self.request.method == 'DELETE':
445451
return NodeContributorDetailSerializer
446452
elif self.request.method == 'POST':
@@ -501,6 +507,18 @@ def get_serializer_context(self):
501507
context['default_email'] = 'default'
502508
return context
503509

510+
def patch(self, request, *args, **kwargs):
511+
"""
512+
Override the default patch behavior to handle the special case
513+
of updating contributors by copying contributors from the parent project.
514+
"""
515+
if request.query_params.get('copy_contributors_from_parent_project'):
516+
instance = self.get_object()
517+
serializer = self.get_serializer_class()(instance, data=request.data)
518+
serializer.is_valid(raise_exception=True)
519+
serializer.save()
520+
return Response(serializer.data, status=200)
521+
return super().patch(request, *args, **kwargs)
504522

505523
class NodeContributorDetail(BaseContributorDetail, generics.RetrieveUpdateDestroyAPIView, NodeMixin, UserMixin):
506524
"""The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/nodes_contributors_read).

api_tests/base/test_utils.py

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from django.conf import settings as django_conf_settings
66

77
from rest_framework import fields
8-
from rest_framework.exceptions import ValidationError
98

109
from api.base import utils as api_utils
1110
from osf.models.base import coerce_guid, Guid, GuidMixin, OptionalGuidMixin, VersionedGuidMixin, InvalidGuid
@@ -63,47 +62,6 @@ def test_push_status_message_no_response(self):
6362
except BaseException:
6463
assert False, f'Exception from push_status_message via API v2 with type "{status}".'
6564

66-
def test_push_status_message_expected_error(self):
67-
status_message = 'This is a message'
68-
try:
69-
push_status_message(status_message, kind='error')
70-
assert False, 'push_status_message() should have generated a ValidationError exception.'
71-
72-
except ValidationError as e:
73-
assert (
74-
e.detail[0] == status_message
75-
), 'push_status_message() should have passed along the message with the Exception.'
76-
77-
except RuntimeError:
78-
assert False, 'push_status_message() should have caught the runtime error and replaced it.'
79-
80-
except BaseException:
81-
assert False, 'Exception from push_status_message when called from the v2 API with type "error"'
82-
83-
@mock.patch('framework.status.get_session')
84-
def test_push_status_message_unexpected_error(self, mock_get_session):
85-
status_message = 'This is a message'
86-
exception_message = 'this is some very unexpected problem'
87-
mock_session = mock.Mock()
88-
mock_session.attach_mock(mock.Mock(side_effect=RuntimeError(exception_message)), 'get')
89-
mock_get_session.return_value = mock_session
90-
try:
91-
push_status_message(status_message, kind='error')
92-
assert False, 'push_status_message() should have generated a RuntimeError exception.'
93-
except ValidationError:
94-
assert False, 'push_status_message() should have re-raised the RuntimeError not gotten ValidationError.'
95-
except RuntimeError as e:
96-
assert str(e) == exception_message, (
97-
'push_status_message() should have re-raised the '
98-
'original RuntimeError with the original message.'
99-
)
100-
101-
except BaseException:
102-
assert False, (
103-
'Unexpected Exception from push_status_message when called '
104-
'from the v2 API with type "error"'
105-
)
106-
10765

10866
@pytest.mark.django_db
10967
class TestCoerceGuid:

api_tests/files/views/test_file_detail.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,8 @@ def test_file_with_wrong_guid(self, app, user):
121121
def test_file_guid_not_created_with_basic_auth(
122122
self, mock_allow, app, user, file_url):
123123
res = app.get(f'{file_url}?create_guid=1', auth=user.auth)
124-
guid = res.json['data']['attributes'].get('guid', None)
125124
assert res.status_code == 200
126125
assert mock_allow.call_count == 1
127-
assert guid is None
128126

129127
@mock.patch('api.base.throttling.CreateGuidThrottle.allow_request')
130128
def test_file_guid_created_with_cookie(

api_tests/metadata_records/test_custom_item_metadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ def test_with_write_permission(self, app, public_osfguid, private_osfguid, anybo
351351
resource_type_general='book-chapter',
352352
),
353353
auth=anybody_with_write_permission.auth,
354+
expect_errors=True,
354355
)
355356
assert res.status_code == 400
356357
expected.assert_expectations(db_record=db_record, api_record=None) # db unchanged

api_tests/nodes/views/test_node_contributors_detail_update.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ def project(self, user, contrib):
3232
)
3333
return project
3434

35+
@pytest.fixture()
36+
def component_project_and_parent_project(self, user, contrib):
37+
# both need to be in same method to keep root same as parent
38+
parent_project = ProjectFactory(creator=user)
39+
parent_project.add_contributor(
40+
contrib,
41+
permissions=permissions.WRITE,
42+
visible=True,
43+
save=True
44+
)
45+
component_project = ProjectFactory(creator=user)
46+
component_project.root = parent_project
47+
component_project.save()
48+
return component_project, parent_project
49+
3550
@pytest.fixture()
3651
def url_creator(self, user, project):
3752
return f'/{API_BASE}nodes/{project._id}/contributors/{user._id}/'
@@ -414,3 +429,22 @@ def test_change_admin_self_with_other_admin(self, app, user, contrib, project, u
414429
attributes = res.json['data']['attributes']
415430
assert attributes['permission'] == permissions.WRITE
416431
assert project.get_permissions(user) == [permissions.READ, permissions.WRITE]
432+
433+
def test_update_component_project_with_parent_contributors(self, app, user, component_project_and_parent_project):
434+
def exists_unique_parent_contributors(parent_project, component_project):
435+
return parent_project.contributors.exclude(
436+
id__in=component_project.contributors.values_list('id', flat=True)
437+
).exists()
438+
component_project, parent_project = component_project_and_parent_project
439+
assert exists_unique_parent_contributors(parent_project, component_project)
440+
res = app.patch_json_api(
441+
f'/{API_BASE}nodes/{component_project._id}/contributors/?copy_contributors_from_parent_project=true',
442+
{
443+
'data': {
444+
'type': 3
445+
}
446+
},
447+
auth=user.auth
448+
)
449+
assert res.status_code == 200
450+
assert not exists_unique_parent_contributors(parent_project, component_project)

osf/models/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1693,7 +1693,7 @@ def get_claim_url(self, project_id, external=False):
16931693
base_url = website_settings.DOMAIN if external else '/'
16941694
unclaimed_record = self.get_unclaimed_record(project_id)
16951695
token = unclaimed_record['token']
1696-
return f'{base_url}user/{uid}/{project_id}/claim/?token={token}'
1696+
return f'{base_url}legacy/user/{uid}/{project_id}/claim/?token={token}'
16971697

16981698
def is_affiliated_with_institution(self, institution):
16991699
"""Return if this user is affiliated with the given ``institution``."""

osf_tests/test_user.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,7 +1165,7 @@ def test_get_claim_url(self, unreg_user, unreg_moderator, project, provider):
11651165
domain = settings.DOMAIN
11661166
assert (
11671167
unreg_user.get_claim_url(pid, external=True) ==
1168-
f'{domain}user/{uid}/{pid}/claim/?token={token}'
1168+
f'{domain}legacy/user/{uid}/{pid}/claim/?token={token}'
11691169
)
11701170

11711171
# test_unreg_moderator
@@ -1175,7 +1175,7 @@ def test_get_claim_url(self, unreg_user, unreg_moderator, project, provider):
11751175
domain = settings.DOMAIN
11761176
assert (
11771177
unreg_moderator.get_claim_url(pid, external=True) ==
1178-
f'{domain}user/{uid}/{pid}/claim/?token={token}'
1178+
f'{domain}legacy/user/{uid}/{pid}/claim/?token={token}'
11791179
)
11801180

11811181
def test_get_claim_url_raises_value_error_if_not_valid_pid(self, unreg_user, unreg_moderator):

tasks/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,6 @@ def test_module(ctx, module=None, numprocesses=None, nocapture=False, params=Non
378378
'api_tests/guids',
379379
'api_tests/meetings',
380380
'api_tests/metadata_records',
381-
'api_tests/osf_groups',
382381
'api_tests/reviews',
383382
'api_tests/regions',
384383
'api_tests/search',

0 commit comments

Comments
 (0)