Skip to content

[ENG-7270] Enable Product Team to Force Archive Registrations in the Admin App #11105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
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
6 changes: 4 additions & 2 deletions admin/nodes/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_share_node/$', views.NodeReindexShare.as_view(), name='reindex-share-node'),
re_path(r'^(?P<guid>[a-z0-9]+)/reindex_elastic_node/$', views.NodeReindexElastic.as_view(),
name='reindex-elastic-node'),
re_path(r'^(?P<guid>[a-z0-9]+)/restart_stuck_registrations/$', views.RestartStuckRegistrationsView.as_view(),
name='restart-stuck-registrations'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove_stuck_registrations/$', views.RemoveStuckRegistrationsView.as_view(),
name='remove-stuck-registrations'),
re_path(r'^(?P<guid>[a-z0-9]+)/check_archive_status/$', views.CheckArchiveStatusRegistrationsView.as_view(),
name='check-archive-status'),
re_path(r'^(?P<guid>[a-z0-9]+)/force_archive_registration/$', views.ForceArchiveRegistrationsView.as_view(),
name='force-archive-registration'),
re_path(r'^(?P<guid>[a-z0-9]+)/remove_user/(?P<user_id>[a-z0-9]+)/$', views.NodeRemoveContributorView.as_view(),
name='remove-user'),
re_path(r'^(?P<guid>[a-z0-9]+)/modify_storage_usage/$', views.NodeModifyStorageUsage.as_view(),
Expand Down
98 changes: 76 additions & 22 deletions admin/nodes/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytz
from enum import Enum
from datetime import datetime
from framework import status

Expand Down Expand Up @@ -26,7 +27,7 @@
from api.share.utils import update_share
from api.caching.tasks import update_storage_usage_cache

from osf.exceptions import NodeStateError
from osf.exceptions import NodeStateError, RegistrationStuckError
from osf.models import (
OSFUser,
NodeLog,
Expand Down Expand Up @@ -672,44 +673,97 @@ def post(self, request, *args, **kwargs):
return redirect(self.get_success_url())


class RestartStuckRegistrationsView(NodeMixin, TemplateView):
""" Allows an authorized user to restart a registrations archive process.
class RemoveStuckRegistrationsView(NodeMixin, View):
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
"""
template_name = 'nodes/restart_registrations_modal.html'
permission_required = ('osf.view_node', 'osf.change_node')

def post(self, request, *args, **kwargs):
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import archive, verify
stuck_reg = self.get_object()
if verify(stuck_reg):
try:
archive(stuck_reg)
messages.success(request, 'Registration archive processes has restarted')
except Exception as exc:
messages.error(request, f'This registration cannot be unstuck due to {exc.__class__.__name__} '
f'if the problem persists get a developer to fix it.')
if Registration.find_failed_registrations().filter(id=stuck_reg.id).exists():
stuck_reg.delete_registration_tree(save=True)
messages.success(request, 'The registration has been deleted')
else:
messages.error(request, 'This registration may not technically be stuck,'
' if the problem persists get a developer to fix it.')

return redirect(self.get_success_url())


class RemoveStuckRegistrationsView(NodeMixin, TemplateView):
""" Allows an authorized user to remove a registrations if it's stuck in the archiving process.
class CheckArchiveStatusRegistrationsView(NodeMixin, View):
"""Allows an authorized user to check a registration archive status.
"""
permission_required = ('osf.view_node', 'osf.change_node')

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

registration = self.get_object()

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

try:
archive_status = check(registration)
messages.success(request, archive_status)
except RegistrationStuckError as exc:
messages.error(request, str(exc))

return redirect(self.get_success_url())


class CollisionMode(Enum):
NONE: str = 'none'
SKIP: str = 'skip'
DELETE: str = 'delete'


class ForceArchiveRegistrationsView(NodeMixin, View):
"""Allows an authorized user to force archive registration.
"""
template_name = 'nodes/remove_registrations_modal.html'
permission_required = ('osf.view_node', 'osf.change_node')

def post(self, request, *args, **kwargs):
stuck_reg = self.get_object()
if Registration.find_failed_registrations().filter(id=stuck_reg.id).exists():
stuck_reg.delete_registration_tree(save=True)
messages.success(request, 'The registration has been deleted')
# Prevents circular imports that cause admin app to hang at startup
from osf.management.commands.force_archive import verify, archive, DEFAULT_PERMISSIBLE_ADDONS

registration = self.get_object()
force_archive_params = request.POST

collision_mode = force_archive_params.get('collision_mode', CollisionMode.NONE.value)
delete_collision = CollisionMode.DELETE.value == collision_mode
skip_collision = CollisionMode.SKIP.value == collision_mode

allow_unconfigured = force_archive_params.get('allow_unconfigured', False)

addons = set(force_archive_params.getlist('addons', []))
addons.update(DEFAULT_PERMISSIBLE_ADDONS)

try:
verify(registration, permissible_addons=addons, raise_error=True)
except ValidationError as exc:
messages.error(request, str(exc))
return redirect(self.get_success_url())

dry_mode = force_archive_params.get('dry_mode', False)

if dry_mode:
messages.success(request, f"Registration {registration._id} can be archived.")
else:
messages.error(request, 'This registration may not technically be stuck,'
' if the problem persists get a developer to fix it.')
try:
archive(
registration,
permissible_addons=addons,
allow_unconfigured=allow_unconfigured,
skip_collision=skip_collision,
delete_collision=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.')

return redirect(self.get_success_url())

Expand Down
2 changes: 1 addition & 1 deletion admin/templates/nodes/node.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<a href="{% url 'nodes:search' %}" class="btn btn-primary"> <i class="fa fa-search"></i></a>
<a href="{% url 'nodes:node-logs' guid=node.guid %}" class="btn btn-primary">View Logs</a>
{% include "nodes/remove_node.html" with node=node %}
{% include "nodes/restart_stuck_registration.html" with node=node %}
{% include "nodes/registration_force_archive.html" with node=node %}
{% include "nodes/make_private.html" with node=node %}
{% include "nodes/make_public.html" with node=node %}
{% include "nodes/mark_spam.html" with node=node %}
Expand Down
79 changes: 79 additions & 0 deletions admin/templates/nodes/registration_force_archive.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{% if node.is_registration %}
<a href="{% url 'nodes:check-archive-status' guid=node.guid %}" class="btn btn-info">
Check archive status
</a>
{% if not node.archived %}
{% if node.is_stuck_registration %}
<a data-toggle="modal" data-target="#confirmRestartStuckRegistration" class="btn btn-danger">
Restart Stuck Registration
</a>
<a href="{% url 'nodes:remove-stuck-registrations' guid=node.guid %}"
data-toggle="modal" data-target="#confirmRemoveRegistration"
class="btn btn-danger">
Remove Stuck Registration
</a>
{% else %}
<a data-toggle="modal" data-target="#confirmForceArchiveRegistration" class="btn btn-danger">
Force Archive
</a>
{% endif %}

<!-- Modals -->
<div class="modal" id="confirmForceArchiveRegistration">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Are you sure you want to force archive the registration? {{ node.guid }}</h3>
</div>
{% include "nodes/registration_force_archive_form.html" with form_id="confirm-force-archive-form" %}
<div class="modal-footer">
<label><input class="form-check-input" type="checkbox" name="dry_mode" id="dry-mode" form="confirm-force-archive-form" value="true">Dry-mode</label>
<input class="btn btn-danger" type="submit" form="confirm-force-archive-form" value="Confirm" />
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</div>
</div>
</div>

<div class="modal" id="confirmRemoveRegistration">
<div class="modal-dialog">
<div class="modal-content">
<form class="well" method="post" action="{% url 'nodes:remove-stuck-registrations' guid=node.guid%}">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Are you sure you want to remove this registration? {{ node.guid }}</h3>
</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>

<div class="modal" id="confirmRestartStuckRegistration">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Are you sure you want to restart the registration process? {{ node.guid }}</h3>
</div>
{% include "nodes/registration_force_archive_form.html" with form_id="confirm-restart-stuck-registration" %}
<div class="modal-footer">
<input class="btn btn-danger" type="submit" form="confirm-restart-stuck-registration" value="Confirm" />
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endif %}
39 changes: 39 additions & 0 deletions admin/templates/nodes/registration_force_archive_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<form id="{{ form_id }}" class="well" method="post" action="{% url 'nodes:force-archive-registration' guid=node.guid%}">
{% csrf_token %}
<div class="modal-content" style="padding: 5px">
<div>
<h4>Handle collision:</h4>
<div>
<div>
<label><input type="radio" name="collision_mode" value="none" checked>None</label>
</div>
<div>
<label><input type="radio" name="collision_mode" value="skip">Skip Collisions</label>
</div>
<div>
<label><input type="radio" name="collision_mode" value="delete">Delete Collisions</label>
</div>
</div>
</div>
<div>
<h4>Permissible Addons (Optional):</h4>
<div>
<div>
{% for addon_name in node.get_addon_names %}
<input class="form-check-input" type="checkbox" name="addons" id="permissibleAddon" value="{{ addon_name }}">
<label class="form-check-label" for="permissibleAddon">{{ addon_name }}</label>
{% endfor %}
</div>
</div>
</div>
<div>
<h4>Other:</h4>
<div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_unconfigured" id="allowUnconfigured">
<label class="form-check-label" for="allowUnconfigured">Allow unconfigured addons</label>
</div>
</div>
</div>
</div>
</form>
51 changes: 0 additions & 51 deletions admin/templates/nodes/restart_stuck_registration.html

This file was deleted.

Loading
Loading