diff --git a/admin/nodes/views.py b/admin/nodes/views.py index a97fec18597..d0372b7dc29 100644 --- a/admin/nodes/views.py +++ b/admin/nodes/views.py @@ -112,7 +112,8 @@ def post(self, request, *args, **kwargs): node.add_log( action=NodeLog.REGISTRATION_DATE_UPDATED, - auth=request, + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, params={ 'last_date': str(last_date), 'new_date': str(new_date) @@ -225,22 +226,18 @@ def post(self, request, *args, **kwargs): message=f'User {user.pk} removed from {node.__class__.__name__.lower()} {node.pk}.', action_flag=CONTRIBUTOR_REMOVED ) - # Log invisibly on the OSF. - self.add_contributor_removed_log(node, user) - return redirect(self.get_success_url()) + params = dict(node.log_params) + params['contributors'] = user.pk + node.add_log( + action=NodeLog.CONTRIB_REMOVED, + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params=params, + log_date=timezone.now(), + should_hide=False, + ) - def add_contributor_removed_log(self, node, user): - NodeLog( - action=NodeLog.CONTRIB_REMOVED, - user=None, - params={ - 'project': node.parent_id, - 'node': node.pk, - 'contributors': user.pk - }, - date=timezone.now(), - should_hide=True, - ).save() + return redirect(self.get_success_url()) class NodeUpdatePermissionsView(NodeMixin, View): @@ -266,6 +263,7 @@ def post(self, request, *args, **kwargs): new_permissions_to_add = data.get('new-permissions', []) new_permission_indexes_to_remove = [] + added_contributor_ids = [] for email, permission in zip(new_emails_to_add, new_permissions_to_add): contributor_user = OSFUser.objects.filter(emails__address=email.lower()).first() if not contributor_user: @@ -281,8 +279,10 @@ def post(self, request, *args, **kwargs): auth=request, user_id=contributor_user._id, permissions=permission, - notification_type=None + notification_type=None, + log=False, ) + added_contributor_ids.append(contributor_user._id) messages.success(self.request, f'User with email {email} was successfully added.') # should remove permissions of invalid emails because @@ -291,6 +291,19 @@ def post(self, request, *args, **kwargs): for permission_index in new_permission_indexes_to_remove: new_permissions_to_add.pop(permission_index) + # Log support-added contributors, if any + if added_contributor_ids: + params = resource.log_params + params['contributors'] = added_contributor_ids + resource.add_log( + action=NodeLog.CONTRIB_ADDED, + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params=params, + log_date=timezone.now(), + should_hide=False, + ) + updated_permissions = data.get('updated-permissions', []) all_permissions = updated_permissions + new_permissions_to_add has_admin = list(filter(lambda permission: ADMIN in permission, all_permissions)) @@ -298,6 +311,7 @@ def post(self, request, *args, **kwargs): messages.error(self.request, 'Must be at least one admin on this node.') return redirect(self.get_success_url()) + permissions_changed = {} for contributor_permission in updated_permissions: guid, permission = contributor_permission.split('-') user = OSFUser.load(guid) @@ -307,7 +321,21 @@ def post(self, request, *args, **kwargs): resource.get_visible(user), request, save=True, - skip_permission=True + skip_permission=True, + log=False, + ) + permissions_changed[user._id] = permission + + if permissions_changed: + params = resource.log_params + params['contributors'] = permissions_changed + resource.add_log( + action=NodeLog.PERMISSIONS_UPDATED, + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params=params, + log_date=timezone.now(), + should_hide=False, ) return redirect(self.get_success_url()) @@ -332,15 +360,14 @@ def post(self, request, *args, **kwargs): message=f'Node {node.pk} restored.', action_flag=NODE_RESTORED ) - NodeLog( + node.add_log( action=NodeLog.NODE_CREATED, - user=None, - params={ - 'project': node.parent_id, - }, - date=timezone.now(), - should_hide=True, - ).save() + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params=dict(node.log_params), + log_date=timezone.now(), + should_hide=False, + ) else: node.is_deleted = True node.deleted = timezone.now() @@ -352,15 +379,14 @@ def post(self, request, *args, **kwargs): message=f'Node {node.pk} removed.', action_flag=NODE_REMOVED ) - NodeLog( + node.add_log( action=NodeLog.NODE_REMOVED, - user=None, - params={ - 'project': node.parent_id, - }, - date=timezone.now(), - should_hide=True, - ).save() + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params=dict(node.log_params), + log_date=timezone.now(), + should_hide=False, + ) node.save() return redirect(self.get_success_url()) @@ -852,6 +878,18 @@ def post(self, request, *args, **kwargs): node.save() + node.add_log( + action=NodeLog.MADE_PRIVATE, + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params={ + 'project': node.parent_id, + 'node': node._primary_key, + }, + log_date=timezone.now(), + should_hide=False, + ) + return redirect(self.get_success_url()) @@ -863,9 +901,21 @@ class NodeMakePublic(NodeMixin, View): def post(self, request, *args, **kwargs): node = self.get_object() try: - node.set_privacy('public') + node.set_privacy('public', auth=None, log=False) except NodeStateError as e: messages.error(request, str(e)) + else: + node.add_log( + action=NodeLog.MADE_PUBLIC, + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params={ + 'project': node.parent_id, + 'node': node._primary_key, + }, + log_date=timezone.now(), + should_hide=False, + ) return redirect(self.get_success_url()) @@ -892,10 +942,26 @@ def _remove_file_from_schema_response_blocks(registration, removed_file_id): # delete file from registration and metadata and keep it for original project if guid and (file := guid.referent) and (file.target == node) and not isinstance(file, TrashedFile): + file_id = file._id + file_path = getattr(file, 'materialized_path', None) or getattr(file, 'path', None) or '' + copied_from_id = getattr(file, 'copied_from_id', None) or getattr(getattr(file, 'copied_from', None), '_id', None) with transaction.atomic(): file.delete() _update_schema_meta(file.target) - _remove_file_from_schema_response_blocks(node, [file._id, file.copied_from._id]) + _remove_file_from_schema_response_blocks(node, [file_id, copied_from_id]) + node.add_log( + action=NodeLog.FILE_REMOVED, + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params={ + 'project': node.parent_id, + 'node': node._primary_key, + 'pathType': 'file', + 'path': file_path, + }, + log_date=timezone.now(), + should_hide=False, + ) return redirect(self.get_success_url()) @@ -932,7 +998,20 @@ def post(self, request, *args, **kwargs): parent=osfstorage.get_root(), name=osfstorage.archive_folder_name ).first() - file.copy_under(archive_folder) + copied = file.copy_under(archive_folder) + copied_path = getattr(copied, 'materialized_path', None) or getattr(copied, 'path', None) or '' + registration.add_log( + action=NodeLog.FILE_ADDED, + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params={ + 'project': registration.parent_id, + 'node': registration._primary_key, + 'path': copied_path, + }, + log_date=timezone.now(), + should_hide=False, + ) messages.success(request, 'The file was successfully added.') return redirect(self.get_success_url()) @@ -959,8 +1038,21 @@ def post(self, request, *args, **kwargs): if not registration_file.exists(): messages.error(request, 'The file with the provided guid is not part of the registration.') return redirect(self.get_success_url()) - + file_path = getattr(file, 'materialized_path', None) or getattr(file, 'path', None) or '' registration_file.delete() + registration.add_log( + action=NodeLog.FILE_REMOVED, + auth=None, + foreign_user=NodeLog.SUPPORT_USER_LABEL, + params={ + 'project': registration.parent_id, + 'node': registration._primary_key, + 'pathType': 'file', + 'path': file_path, + }, + log_date=timezone.now(), + should_hide=False, + ) messages.success(request, 'The file was successfully removed.') return redirect(self.get_success_url()) diff --git a/admin_tests/nodes/test_views.py b/admin_tests/nodes/test_views.py index a81d1102b7a..7b5a7f236c3 100644 --- a/admin_tests/nodes/test_views.py +++ b/admin_tests/nodes/test_views.py @@ -269,10 +269,11 @@ def test_do_not_remove_last_admin(self): assert len(list(self.node.get_admin_contributors(self.node.contributors))) == 1 assert AdminLogEntry.objects.count() == count - def test_no_log(self): + def test_log(self): + assert not self.node.logs.filter(action=NodeLog.CONTRIB_REMOVED).exists() view = setup_log_view(self.view(), self.request, guid=self.node._id, user_id=self.user_2.id) view.post(self.request) - assert self.node.logs.latest().action != NodeLog.CONTRIB_REMOVED + assert self.node.logs.filter(action=NodeLog.CONTRIB_REMOVED).exists() def test_no_user_permissions_raises_error(self): guid = self.node._id diff --git a/admin_tests/preprints/test_views.py b/admin_tests/preprints/test_views.py index 5582c1b07eb..02519290733 100644 --- a/admin_tests/preprints/test_views.py +++ b/admin_tests/preprints/test_views.py @@ -542,7 +542,8 @@ def test_do_not_remove_last_admin(self): assert len(list(self.preprint.get_admin_contributors(self.preprint.contributors))) == 1 assert AdminLogEntry.objects.count() == count - def test_no_log(self): + def test_log(self): + assert not self.preprint.logs.filter(action=PreprintLog.CONTRIB_REMOVED).exists() view = setup_log_view( self.view(), self.request, @@ -550,7 +551,7 @@ def test_no_log(self): user_id=self.user_2.id ) view.post(self.request) - assert self.preprint.logs.latest().action != PreprintLog.CONTRIB_REMOVED + assert self.preprint.logs.filter(action=PreprintLog.CONTRIB_REMOVED).exists() @pytest.mark.urls('admin.base.urls') diff --git a/api/logs/serializers.py b/api/logs/serializers.py index 51567b991f3..7768074c6e0 100644 --- a/api/logs/serializers.py +++ b/api/logs/serializers.py @@ -203,11 +203,13 @@ class NodeLogSerializer(JSONAPISerializer): 'id', 'date', 'action', + 'foreign_user', ] id = ser.CharField(read_only=True, source='_id') date = VersionedDateTimeField(read_only=True) action = ser.CharField(read_only=True) + foreign_user = ser.CharField(read_only=True) params = ser.SerializerMethodField(read_only=True) links = LinksField({'self': 'get_absolute_url'}) diff --git a/osf/models/mixins.py b/osf/models/mixins.py index 692075ef4c6..1d0f2a70a83 100644 --- a/osf/models/mixins.py +++ b/osf/models/mixins.py @@ -1796,7 +1796,7 @@ def copy_unclaimed_records(self, resource): contributor.save() # TODO: optimize me - def update_contributor(self, user, permission, visible, auth, save=False, skip_permission=False): + def update_contributor(self, user, permission, visible, auth, save=False, skip_permission=False, log=True): """ TODO: this method should be updated as a replacement for the main loop of Node#manage_contributors. Right now there are redundancies, but to avoid major feature creep this will not be included as this time. @@ -1823,17 +1823,18 @@ def update_contributor(self, user, permission, visible, auth, save=False, skip_p ) if not self.get_group(permission).user_set.filter(id=user.id).exists(): self.set_permissions(user, permission, save=False) - permissions_changed = { - user._id: permission - } - params = self.log_params - params['contributors'] = permissions_changed - self.add_log( - action=self.log_class.PERMISSIONS_UPDATED, - params=params, - auth=auth, - save=False - ) + if log: + permissions_changed = { + user._id: permission + } + params = self.log_params + params['contributors'] = permissions_changed + self.add_log( + action=self.log_class.PERMISSIONS_UPDATED, + params=params, + auth=auth, + save=False + ) with transaction.atomic(): if [READ] in permissions_changed.values(): project_signals.write_permissions_revoked.send(self) diff --git a/osf/models/nodelog.py b/osf/models/nodelog.py index 6aa14f461e5..c40390ed6d4 100644 --- a/osf/models/nodelog.py +++ b/osf/models/nodelog.py @@ -16,6 +16,7 @@ class NodeLog(ObjectIDMixin, BaseModel): } DATE_FORMAT = '%m/%d/%Y %H:%M UTC' + SUPPORT_USER_LABEL = 'an OSF Support Team Member' # Log action constants -- NOTE: templates stored in log_templates.mako CREATED_FROM = 'created_from'