Skip to content
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

[ENG-6946] Ability to approve registration requests for approval #10972

Open
wants to merge 4 commits into
base: feature/b-and-i-25-01
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions admin/nodes/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
re_path(r'^known_spam$', views.NodeKnownSpamList.as_view(), name='known-spam'),
re_path(r'^known_ham$', views.NodeKnownHamList.as_view(), name='known-ham'),
re_path(r'^doi_backlog_list/$', views.DoiBacklogListView.as_view(), name='doi-backlog-list'),
re_path(r'^approval_backlog_list/$', views.ApprovalBacklogListView.as_view(), name='approval-backlog-list'),
re_path(r'^confirm_approve_backlog_list/$', views.ConfirmApproveBacklogView.as_view(), name='confirm-approve-backlog-list'),
re_path(r'^registration_list/$', views.RegistrationListView.as_view(), name='registrations'),
re_path(r'^stuck_registration_list/$', views.StuckRegistrationListView.as_view(), name='stuck-registrations'),
re_path(r'^ia_backlog_list/$', views.RegistrationBacklogListView.as_view(), name='ia-backlog-list'),
Expand Down
38 changes: 38 additions & 0 deletions admin/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
NodeLog,
AbstractNode,
Registration,
RegistrationApproval,
SpamStatus
)
from osf.models.admin_log_entry import (
Expand All @@ -47,6 +48,8 @@
)
from osf.utils.permissions import ADMIN

from scripts.approve_registrations import approve_past_pendings

from website import settings, search


Expand Down Expand Up @@ -335,6 +338,41 @@ def get_queryset(self):
return Registration.find_doi_backlog().annotate(guid=F('guids___id'))


class ApprovalBacklogListView(RegistrationListView):
""" Allows authorized users to view a list of registrations that have not yet been approved.
"""
template_name = 'nodes/registration_approval_list.html'
permission_required = 'osf.view_registrationapproval'

def get_queryset(self):
# Django template does not like attributes with underscores for some reason, so we annotate.
return RegistrationApproval.find_approval_backlog()

def get_context_data(self, **kwargs):
queryset = self.get_queryset()
page_size = self.get_paginate_by(queryset)
paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
return {
'queryset': queryset,
'page': page,
}


class ConfirmApproveBacklogView(RegistrationListView):
template_name = 'nodes/registration_approval_list.html'
permission_required = 'osf.view_registrationapproval'

def get_success_url(self):
return reverse('nodes:approval-backlog-list')

def post(self, request, *args, **kwargs):
data = dict(request.POST)
data.pop('csrfmiddlewaretoken', None)
approvals = RegistrationApproval.objects.filter(_id__in=list(data.keys()))
approve_past_pendings(approvals, dry_run=False)
return redirect(self.get_success_url())


class RegistrationUpdateEmbargoView(NodeMixin, View):
""" Allows authorized users to update the embargo of a registration.
"""
Expand Down
1 change: 1 addition & 0 deletions admin/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
<li><a href="{% url 'nodes:stuck-registrations' %}"><i class='fa fa-link'></i> <span>Stuck Registrations</span></a></li>
<li><a href="{% url 'nodes:ia-backlog-list' %}"><i class='fa fa-link'></i> <span>IA Backlog</span></a></li>
<li><a href="{% url 'nodes:doi-backlog-list' %}"><i class='fa fa-link'></i> <span>DOI Backlog</span></a></li>
<li><a href="{% url 'nodes:approval-backlog-list' %}"><i class='fa fa-link'></i> <span>Approval Backlog</span></a></li>
</ul>
</div>
{% endif %}
Expand Down
22 changes: 22 additions & 0 deletions admin/templates/nodes/approve_modal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<a data-toggle="modal" data-target="#confirmApproveRegistrationListModal" class="btn btn-success">
Approve
</a>
<div class="modal" id="confirmApproveRegistrationListModal">
<div class="modal-dialog">
<div class="modal-content">
<form class="well" method="post" action="{% url 'nodes:confirm-approve-backlog-list' %}">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">x</button>
<h3>Are you sure you want to approve these registrations?</h3>
</div>
{% csrf_token %}
<div class="modal-footer">
<input class="btn btn-success" type="submit" value="Confirm" />
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
76 changes: 76 additions & 0 deletions admin/templates/nodes/registration_approval_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{% extends "base.html" %}
{% load render_bundle from webpack_loader %}
{% load node_extras %}

{% load static %}
{% block title %}
<title>Registrations to be approved</title>
{% endblock title %}
{% block content %}
<h2>List of registration requests that are waiting for approval</h2>
{% if perms.osf.change_registrationapproval %}
<form action="{% url 'nodes:confirm-approve-backlog-list' %}" method="post">
{% csrf_token %}
{% endif %}
{% include "util/pagination.html" with items=page status=status %}
<table class="table table-striped table-hover table-responsive">
<thead>
<tr>
<th>
<input type="checkbox" onclick="toggle(this)">
<script language="javascript">
function toggle(source) {
var checkboxes = document.getElementsByClassName('selection');
for (var i in checkboxes) {
checkboxes[i].checked = source.checked;
}
}
</script>
</th>
<th>ID</th>
<th>Title</th>
<th>Date created</th>
<th>Initiated By</th>
<th>State</th>
<th>Initiation Date</th>
<th>End Date</th>
</tr>
</thead>
<tbody>
{% for approval in queryset %}
<tr>
{% if perms.osf.change_registrationapproval %}
<td>
<input name="{{approval.guid}}" class="selection" type="checkbox"/>
</td>
{% endif %}
<td>
{{ approval.guid }}
</td>
<td>
{{ approval.registrations.first.title }}
</td>
<td>
{{ approval.created| date }}
</td>
<td>
{{ approval.initiated_by }}
</td>
<td>
{{ approval.state }}
</td>
<td>
{{ approval.initiation_date }}
</td>
<td>
{{ approval.end_date }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if perms.osf.change_registrationapproval %}
{% include 'nodes/approve_modal.html'%}
</form>
{% endif %}
{% endblock content %}
1 change: 1 addition & 0 deletions admin/templates/nodes/registration_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
{% endblock title %}
{% block content %}
<h2>List of Registrations</h2>
{% include "util/pagination.html" with items=page status=status %}
<table class="table table-striped table-hover table-responsive">
<thead>
<tr>
Expand Down
65 changes: 62 additions & 3 deletions admin_tests/nodes/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytz
import datetime

from osf.models import AdminLogEntry, NodeLog, AbstractNode
from osf.models import AdminLogEntry, NodeLog, AbstractNode, RegistrationApproval
from admin.nodes.views import (
NodeDeleteView,
NodeRemoveContributorView,
Expand All @@ -17,7 +17,9 @@
NodeConfirmHamView,
AdminNodeLogView,
RestartStuckRegistrationsView,
RemoveStuckRegistrationsView
RemoveStuckRegistrationsView,
ApprovalBacklogListView,
ConfirmApproveBacklogView
)
from admin_tests.utilities import setup_log_view, setup_view
from api_tests.share._utils import mock_update_share
Expand All @@ -31,7 +33,9 @@
from framework.auth.core import Auth

from tests.base import AdminTestCase
from osf_tests.factories import UserFactory, AuthUserFactory, ProjectFactory, RegistrationFactory
from osf_tests.factories import UserFactory, AuthUserFactory, ProjectFactory, RegistrationFactory, RegistrationApprovalFactory

from website.settings import REGISTRATION_APPROVAL_TIME


def patch_messages(request):
Expand Down Expand Up @@ -431,3 +435,58 @@ def test_remove_stuck_registration_with_an_addon(self):
self.registration.refresh_from_db()
assert self.registration.is_deleted
assert self.registration.deleted is not None


class TestApprovalBacklogListView(AdminTestCase):

def setUp(self):
super().setUp()
self.user = AuthUserFactory()
self.node = ProjectFactory(creator=self.user)
self.request = RequestFactory().post('/fake_path')
self.view = setup_log_view(ApprovalBacklogListView(), self.request)

def request_approval(self, timedelta, should_display=False):
now = timezone.now()
RegistrationApprovalFactory(
initiation_date=now + timedelta,
end_date=now + timedelta + REGISTRATION_APPROVAL_TIME
)
res = self.view.get(self.request)
is_displayed_in_queryset = res.context_data['queryset'].exists()
assert is_displayed_in_queryset is should_display

def test_not_expired_approvals_are_shown(self):
# we show all approvals in admin if now <= end_date (approval did not expire)
# as end_date = initiation_date + REGISTRATION_APPROVAL_TIME
self.request_approval(timezone.timedelta(days=-3), should_display=False)
self.request_approval(timezone.timedelta(days=-2), should_display=False)
self.request_approval(timezone.timedelta(days=-1, hours=-23, minutes=-59), should_display=True)
self.request_approval(timezone.timedelta(days=-1, hours=-23, minutes=-59, seconds=59), should_display=True)
self.request_approval(timezone.timedelta(days=-1), should_display=True)
self.request_approval(timezone.timedelta(minutes=-15), should_display=True)
self.request_approval(timezone.timedelta(minutes=15), should_display=True)
self.request_approval(timezone.timedelta(days=1), should_display=True)
self.request_approval(timezone.timedelta(days=1, hours=23, minutes=59), should_display=True)
self.request_approval(timezone.timedelta(days=2), should_display=True)
self.request_approval(timezone.timedelta(days=3), should_display=True)


class TestConfirmApproveBacklogView(AdminTestCase):

def setUp(self):
super().setUp()
self.user = AuthUserFactory()
self.node = ProjectFactory(creator=self.user)

def test_request_approval_is_approved(self):
now = timezone.now()
self.approval = RegistrationApprovalFactory(
initiation_date=now - timezone.timedelta(days=1),
end_date=now + timezone.timedelta(days=1)
)
assert RegistrationApproval.objects.first().state == RegistrationApproval.UNAPPROVED
request = RequestFactory().post('/fake_path', data={f'{self.approval._id}': '[on]'})
view = setup_log_view(ConfirmApproveBacklogView(), request)
view.post(request)
assert RegistrationApproval.objects.first().state == RegistrationApproval.APPROVED
12 changes: 12 additions & 0 deletions osf/models/sanctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,18 @@ class RegistrationApproval(SanctionCallbackMixin, EmailApprovableSanction):

initiated_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE)

@staticmethod
def find_approval_backlog():
"""
These are registration requests that are waiting for approval within REGISTRATION_APPROVAL_TIME time
"""
return RegistrationApproval.objects.filter(
state=RegistrationApproval.UNAPPROVED,
end_date__gte=timezone.now()
).annotate(
guid=models.F('_id')
).order_by('-initiation_date')

def _get_registration(self):
return self.registrations.first()

Expand Down
15 changes: 9 additions & 6 deletions scripts/approve_registrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@
logging.basicConfig(level=logging.INFO)


def main(dry_run=True):
approvals_past_pending = models.RegistrationApproval.objects.filter(
state=models.RegistrationApproval.UNAPPROVED,
initiation_date__lt=timezone.now() - settings.REGISTRATION_APPROVAL_TIME
)

def approve_past_pendings(approvals_past_pending, dry_run=True):
for registration_approval in approvals_past_pending:
if dry_run:
logger.warning('Dry run mode')
Expand Down Expand Up @@ -73,6 +68,14 @@ def main(dry_run=True):
transaction.savepoint_rollback(sid)


def main(dry_run=True):
approvals_past_pending = models.RegistrationApproval.objects.filter(
state=models.RegistrationApproval.UNAPPROVED,
initiation_date__lt=timezone.now() - settings.REGISTRATION_APPROVAL_TIME
)
approve_past_pendings(approvals_past_pending, dry_run)


@celery_app.task(name='scripts.approve_registrations')
def run_main(dry_run=True):
if not dry_run:
Expand Down
Loading