Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions admin/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from scripts.approve_registrations import approve_past_pendings

from website import settings, search
from website.archiver.tasks import force_archive


class NodeMixin(PermissionRequiredMixin):
Expand Down Expand Up @@ -705,6 +706,12 @@ class NodeReindexShare(NodeMixin, View):
def post(self, request, *args, **kwargs):
node = self.get_object()
update_share(node)
messages.success(
request,
'Reindex request has been sent to SHARE. '
'Changes typically appear in OSF Search within about 5 minutes, '
'subject to background queue load and SHARE availability.'
)
update_admin_log(
user_id=self.request.user.id,
object_id=node._id,
Expand Down Expand Up @@ -830,16 +837,19 @@ class CheckArchiveStatusRegistrationsView(NodeMixin, View):

def get(self, request, *args, **kwargs):
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import check
from osf.management.commands.force_archive import check, DEFAULT_PERMISSIBLE_ADDONS

registration = self.get_object()

if registration.archived:
messages.success(request, f"Registration {registration._id} is archived.")
return redirect(self.get_success_url())

addons = set(registration.registered_from.get_addon_names())
addons.update(DEFAULT_PERMISSIBLE_ADDONS)

try:
archive_status = check(registration)
archive_status = check(registration, permissible_addons=addons)
messages.success(request, archive_status)
except RegistrationStuckError as exc:
messages.error(request, str(exc))
Expand All @@ -860,7 +870,7 @@ class ForceArchiveRegistrationsView(NodeMixin, View):

def post(self, request, *args, **kwargs):
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import verify, archive, DEFAULT_PERMISSIBLE_ADDONS
from osf.management.commands.force_archive import verify, DEFAULT_PERMISSIBLE_ADDONS

registration = self.get_object()
force_archive_params = request.POST
Expand All @@ -885,18 +895,14 @@ def post(self, request, *args, **kwargs):
if dry_mode:
messages.success(request, f"Registration {registration._id} can be archived.")
else:
try:
archive(
registration,
permissible_addons=addons,
allow_unconfigured=allow_unconfigured,
skip_collisions=skip_collision,
delete_collisions=delete_collision,
)
messages.success(request, 'Registration archive process has finished.')
except Exception as exc:
messages.error(request, f'This registration cannot be archived due to {exc.__class__.__name__}: {str(exc)}. '
f'If the problem persists get a developer to fix it.')
force_archive_task = force_archive.delay(
str(registration._id),
permissible_addons=list(addons),
allow_unconfigured=allow_unconfigured,
skip_collisions=skip_collision,
delete_collisions=delete_collision,
)
messages.success(request, f'Registration archive process has started. Task id: {force_archive_task.id}.')

return redirect(self.get_success_url())

Expand Down
6 changes: 6 additions & 0 deletions admin/preprints/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ class PreprintReindexShare(PreprintMixin, View):
def post(self, request, *args, **kwargs):
preprint = self.get_object()
update_share(preprint)
messages.success(
request,
'Reindex request has been sent to SHARE. '
'Changes typically appear in OSF Search within about 5 minutes, '
'subject to background queue load and SHARE availability.'
)
update_admin_log(
user_id=self.request.user.id,
object_id=preprint._id,
Expand Down
21 changes: 21 additions & 0 deletions admin/templates/users/reindex_user_share.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<a data-toggle="modal" data-target="#confirmReindexShareUser" class="btn btn-default">SHARE Reindex User Content</a>
<div class="modal" id="confirmReindexShareUser">
<div class="modal-dialog">
<div class="modal-content">
<form class="well" method="post" action="{% url 'users:reindex-share-user' guid=user.guid %}">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Are you sure you want to SHARE reindex all content for this user? {{ user.guid }}</h3>
<p>This will trigger SHARE reindexing for all nodes and preprints where this user is a contributor.</p>
</div>
{% csrf_token %}
<div class="modal-footer">
<input class="btn btn-danger" type="submit" value="Confirm" />
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
1 change: 1 addition & 0 deletions admin/templates/users/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
{% include "users/disable_user.html" with user=user %}
{% include "users/mark_spam.html" with user=user %}
{% include "users/reindex_user_elastic.html" with user=user %}
{% include "users/reindex_user_share.html" with user=user %}
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions admin/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
re_path(r'^(?P<guid>[a-z0-9]+)/get_reset_password/$', views.GetPasswordResetLink.as_view(), name='get-reset-password'),
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_elastic_user/$', views.UserReindexElastic.as_view(),
name='reindex-elastic-user'),
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_share_user/$', views.UserShareReindex.as_view(),
name='reindex-share-user'),
re_path(r'^(?P<guid>[a-z0-9]+)/merge_accounts/$', views.UserMergeAccounts.as_view(), name='merge-accounts'),
re_path(r'^(?P<guid>[a-z0-9]+)/draft_registrations/$', views.UserDraftRegistrationsList.as_view(), name='draft-registrations'),
]
39 changes: 39 additions & 0 deletions admin/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
CONFIRM_HAM,
UNFLAG_SPAM,
REINDEX_ELASTIC,
REINDEX_SHARE,
)

from admin.users.forms import (
Expand Down Expand Up @@ -560,6 +561,44 @@ def post(self, request, *args, **kwargs):
return redirect(self.get_success_url())


class UserShareReindex(UserMixin, View):
permission_required = 'osf.change_osfuser'

def post(self, request, *args, **kwargs):
from api.share.utils import update_share
user = self.get_object()

nodes_count = user.contributed.count()
preprints_count = user.preprints.filter(deleted=None).count()

for node in user.contributed:
try:
update_share(node)
except Exception as e:
messages.error(request, f'Failed to SHARE reindex node {node._id}: {e}')

for preprint in user.preprints.filter(deleted=None):
try:
update_share(preprint)
except Exception as e:
messages.error(request, f'Failed to SHARE reindex preprint {preprint._id}: {e}')

messages.success(
request,
f'Triggered SHARE reindexing for {nodes_count} nodes and {preprints_count} preprints'
)

update_admin_log(
user_id=self.request.user.id,
object_id=user._id,
object_repr='User',
message=f'SHARE reindexed all content for user {user._id}',
action_flag=REINDEX_SHARE
)

return redirect(self.get_success_url())


class UserDraftRegistrationsList(UserMixin, ListView):
template_name = 'users/draft-registrations.html'
permission_required = 'osf.view_draftregistration'
Expand Down
1 change: 1 addition & 0 deletions admin_tests/nodes/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ class TestNodeReindex(AdminTestCase):
def setUp(self):
super().setUp()
self.request = RequestFactory().post('/fake_path')
patch_messages(self.request)

self.user = AuthUserFactory()
self.node = ProjectFactory(creator=self.user)
Expand Down
1 change: 1 addition & 0 deletions admin_tests/preprints/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ def test_reindex_preprint_share(self, preprint, req, mock_update_share):
preprint.provider.save()

count = AdminLogEntry.objects.count()
patch_messages(req)
view = views.PreprintReindexShare()
view = setup_log_view(view, req, guid=preprint._id)
mock_update_share.reset_mock()
Expand Down
2 changes: 2 additions & 0 deletions api/base/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ def get_object_or_error(model_or_qs, query_or_pk=None, request=None, display_nam
# users who are unconfirmed or unregistered, but not users who have been
# disabled.
if model_cls is OSFUser and obj.is_disabled:
if getattr(obj, 'gdpr_deleted', False):
raise NotFound
raise UserGone(user=obj)
if check_deleted and (model_cls is not OSFUser and not getattr(obj, 'is_active', True) or getattr(obj, 'is_deleted', False) or getattr(obj, 'deleted', False)):
if display_name is None:
Expand Down
13 changes: 7 additions & 6 deletions api/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from api.nodes.permissions import ExcludeWithdrawals
from api.users.serializers import UserSerializer
from framework.auth.oauth_scopes import CoreScopes
from osf.models import Contributor, MaintenanceState, BaseFileNode
from osf.models import Contributor, MaintenanceState, BaseFileNode, AbstractNode
from osf.utils.permissions import API_CONTRIBUTOR_PERMISSIONS, READ, WRITE, ADMIN
from waffle.models import Flag, Switch, Sample
from waffle import sample_is_active
Expand Down Expand Up @@ -600,7 +600,7 @@ def get_queryset(self):
)


class BaseLinkedList(JSONAPIBaseView, generics.ListAPIView):
class BaseLinkedList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):

permission_classes = (
drf_permissions.IsAuthenticatedOrReadOnly,
Expand All @@ -618,11 +618,9 @@ class BaseLinkedList(JSONAPIBaseView, generics.ListAPIView):
view_name = None

ordering = ('-modified',)
model_class = AbstractNode

# TODO: This class no longer exists
# model_class = Pointer

def get_queryset(self):
def get_default_queryset(self):
auth = get_user_auth(self.request)
from api.resources import annotations as resource_annotations

Expand All @@ -639,6 +637,9 @@ def get_queryset(self):
.order_by('-modified')
)

def get_queryset(self):
return self.get_queryset_from_request()


class WaterButlerMixin:

Expand Down
9 changes: 7 additions & 2 deletions api/collections/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ class LinkedNodesList(JSONAPIBaseView, generics.ListAPIView, CollectionMixin, No
view_name = 'linked-nodes'

ordering = ('-modified',)
model_class = Node

def get_default_queryset(self):
auth = get_user_auth(self.request)
Expand All @@ -589,7 +590,7 @@ def get_parser_context(self, http_request):
return res


class LinkedRegistrationsList(JSONAPIBaseView, generics.ListAPIView, CollectionMixin):
class LinkedRegistrationsList(JSONAPIBaseView, generics.ListAPIView, CollectionMixin, ListFilterMixin):
"""List of registrations linked to this node. *Read-only*.

Linked registrations are the registration nodes pointed to by node links.
Expand Down Expand Up @@ -667,8 +668,9 @@ class LinkedRegistrationsList(JSONAPIBaseView, generics.ListAPIView, CollectionM
required_write_scopes = [CoreScopes.COLLECTED_META_WRITE]

ordering = ('-modified',)
model_class = Registration

def get_queryset(self):
def get_default_queryset(self):
auth = get_user_auth(self.request)
return Registration.objects.filter(
guids__in=self.get_collection().active_guids.all(),
Expand All @@ -680,6 +682,9 @@ def get_queryset(self):
'-modified',
)

def get_queryset(self):
return self.get_queryset_from_request()

# overrides APIView
def get_parser_context(self, http_request):
"""
Expand Down
8 changes: 6 additions & 2 deletions api/institutions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ class InstitutionNodesRelationship(JSONAPIBaseView, generics.RetrieveDestroyAPIV
}
Success: 204

This requires write permissions in the nodes requested.
This requires write permissions in the nodes requested. If the user has admin permissions on the node,
the institution does not need to be affiliated in their account.
"""
permission_classes = (
drf_permissions.IsAuthenticatedOrReadOnly,
Expand Down Expand Up @@ -367,16 +368,19 @@ def get_object(self):
def perform_destroy(self, instance):
data = self.request.data['data']
user = self.request.user
inst = instance['self']
ids = [datum['id'] for datum in data]
nodes = []
for id_ in ids:
node = Node.load(id_)
if not node.has_permission(user, osf_permissions.WRITE):
raise exceptions.PermissionDenied(detail=f'Write permission on node {id_} required')
if not user.is_affiliated_with_institution(inst) and not node.has_permission(user, osf_permissions.ADMIN):
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}')
nodes.append(node)

for node in nodes:
node.remove_affiliated_institution(inst=instance['self'], user=user)
node.remove_affiliated_institution(inst=inst, user=user)
node.save()

def create(self, *args, **kwargs):
Expand Down
6 changes: 5 additions & 1 deletion api/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,17 @@ def get_serializer_context(self):
return context

def perform_destroy(self, instance):
node = self.get_resource()
node: Node = self.get_resource()
auth = get_user_auth(self.request)
if node.visible_contributors.count() == 1 and instance.visible:
raise ValidationError('Must have at least one visible contributor')
removed = node.remove_contributor(instance, auth)
if not removed:
raise ValidationError('Must have at least one registered admin contributor')
propagate = self.request.query_params.get('propagate_to_children') == 'true'
if propagate:
for child_node in node.get_nodes(_contributors__in=[instance.user]):
child_node.remove_contributor(instance, auth)


class NodeImplicitContributorsList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin, NodeMixin):
Expand Down
3 changes: 2 additions & 1 deletion api/preprints/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ def update(self, preprint, validated_data):

updated_has_prereg_links = validated_data.get('has_prereg_links', preprint.has_prereg_links)
updated_why_no_prereg = validated_data.get('why_no_prereg', preprint.why_no_prereg)
prereg_links = validated_data.get('prereg_links', preprint.prereg_links)

if updated_has_coi is False and updated_conflict_statement:
raise exceptions.ValidationError(
Expand All @@ -342,7 +343,7 @@ def update(self, preprint, validated_data):
detail='Cannot provide data links when has_data_links is set to "no".',
)

if updated_has_prereg_links != 'no' and updated_why_no_prereg:
if updated_has_prereg_links != 'no' and (updated_why_no_prereg and not prereg_links):
raise exceptions.ValidationError(
detail='You cannot edit this statement while your prereg links availability is set to true or is unanswered.',
)
Expand Down
15 changes: 15 additions & 0 deletions api/share/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import logging

from django.apps import apps
from celery.utils.time import get_exponential_backoff_interval
import requests

from framework.celery_tasks import app as celery_app
Expand Down Expand Up @@ -97,6 +98,20 @@ def task__update_share(self, guid: str, is_backfill=False, osfmap_partition_name
_response.raise_for_status()
except Exception as e:
log_exception(e)
if _response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
retry_after = _response.headers.get('Retry-After')
try:
countdown = int(retry_after)
except (TypeError, ValueError):
retries = getattr(self.request, 'retries', 0)
countdown = get_exponential_backoff_interval(
factor=4,
retries=retries,
maximum=2 * 60,
full_jitter=True,
)
raise self.retry(exc=e, countdown=countdown)

if HTTPStatus(_response.status_code).is_server_error:
raise self.retry(exc=e)
else: # success response
Expand Down
Loading
Loading