Skip to content

Commit 8745da4

Browse files
authored
[ENG-8462] Institution setup fixes (#11241)
* create monthly reports when institution is created * better exception handling; async generate report * fix test * handle deactivated institutions; minor fixes
1 parent 8d9a26f commit 8745da4

File tree

9 files changed

+133
-10
lines changed

9 files changed

+133
-10
lines changed

admin/institutions/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
re_path(r'^import/$', views.ImportInstitution.as_view(), name='import'),
1010
re_path(r'^(?P<institution_id>[0-9]+)/$', views.InstitutionDetail.as_view(), name='detail'),
1111
re_path(r'^(?P<institution_id>[0-9]+)/export/$', views.InstitutionExport.as_view(), name='export'),
12+
re_path(r'^(?P<institution_id>[0-9]+)/monthly_report/$', views.InstitutionMonthlyReporterDo.as_view(), name='monthly_report'),
1213
re_path(r'^(?P<institution_id>[0-9]+)/delete/$', views.DeleteInstitution.as_view(), name='delete'),
1314
re_path(r'^(?P<institution_id>[0-9]+)/deactivate/$', views.DeactivateInstitution.as_view(), name='deactivate'),
1415
re_path(r'^(?P<institution_id>[0-9]+)/reactivate/$', views.ReactivateInstitution.as_view(), name='reactivate'),

admin/institutions/views.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
from dateutil.parser import isoparse
23

34
from django.contrib import messages
45
from django.contrib.auth.mixins import PermissionRequiredMixin
@@ -15,6 +16,9 @@
1516
from admin.base.forms import ImportFileForm
1617
from admin.institutions.forms import InstitutionForm, InstitutionalMetricsAdminRegisterForm
1718
from osf.models import Institution, Node, OSFUser
19+
from osf.metrics.utils import YearMonth
20+
from osf.metrics.reporters import AllMonthlyReporters
21+
from osf.management.commands.monthly_reporters_go import monthly_reporter_do
1822

1923

2024
class InstitutionList(PermissionRequiredMixin, ListView):
@@ -129,6 +133,38 @@ def get(self, request, *args, **kwargs):
129133
return response
130134

131135

136+
class InstitutionMonthlyReporterDo(PermissionRequiredMixin, View):
137+
permission_required = 'osf.view_institution'
138+
raise_exception = True
139+
140+
def post(self, request, *args, **kwargs):
141+
institution_id = self.kwargs.get('institution_id')
142+
try:
143+
institution = Institution.objects.get_all_institutions().get(id=institution_id)
144+
except Institution.DoesNotExist:
145+
raise Http404(f"Institution with id {institution_id} is not found or deactivated.")
146+
147+
monthly_report_date = request.POST.get('monthly_report_date', None)
148+
if monthly_report_date:
149+
try:
150+
monthly_report_date = isoparse(monthly_report_date).date()
151+
except ValueError as exc:
152+
messages.error(request, str(exc))
153+
return redirect('institutions:detail', institution_id=institution.id)
154+
else:
155+
messages.error(request, 'Report date cannot be none.')
156+
return redirect('institutions:detail', institution_id=institution.id)
157+
158+
monthly_reporter_do.apply_async(kwargs={
159+
'yearmonth': str(YearMonth.from_date(monthly_report_date)),
160+
'reporter_key': request.POST.get('monthly_reporter', None),
161+
'report_kwargs': {'institution_pk': institution.id},
162+
})
163+
164+
messages.success(request, 'Monthly reporter successfully went.')
165+
return redirect('institutions:detail', institution_id=institution.id)
166+
167+
132168
class CreateInstitution(PermissionRequiredMixin, CreateView):
133169
permission_required = 'osf.change_institution'
134170
raise_exception = True
@@ -141,6 +177,18 @@ def get_context_data(self, *args, **kwargs):
141177
kwargs['import_form'] = ImportFileForm()
142178
return super().get_context_data(*args, **kwargs)
143179

180+
def form_valid(self, form):
181+
response = super().form_valid(form)
182+
183+
# Make a report after Institution is created
184+
monthly_reporter_do.apply_async(kwargs={
185+
'yearmonth': str(YearMonth.from_date(self.object.created)),
186+
'reporter_key': AllMonthlyReporters.INSTITUTIONAL_SUMMARY.name,
187+
'report_kwargs': {'institution_pk': self.object.id},
188+
})
189+
190+
return response
191+
144192

145193
class InstitutionNodeList(PermissionRequiredMixin, ListView):
146194
template_name = 'institutions/node_list.html'

admin/management/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def post(self, request, *args, **kwargs):
130130
if report_date is not None
131131
else ''
132132
),
133+
reporter_key=request.POST.get('monthly_reporter', '')
133134
)
134135

135136
if errors:

admin/templates/institutions/detail.html

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@
99
{% endblock title %}
1010
{% block content %}
1111
<div class="container-fluid">
12+
<ul class="messages">
13+
{% for message in messages %}
14+
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
15+
{% endfor %}
16+
</ul>
1217
<div class="row">
1318
<div class="col-md-12">
1419
<a class="btn btn-primary" href={% url 'institutions:export' institution.id %}>Export institution metadata</a>
20+
<a data-toggle="modal" data-target="#monthlyReportModal" class="btn btn-primary">
21+
Run monthly report
22+
</a>
1523
{% if perms.osf.delete_institution %}
1624
<a class="btn btn-danger" href={% url 'institutions:delete' institution.id %}>Delete institution</a>
1725
{% endif %}
@@ -100,6 +108,33 @@ <h4>Import from JSON</h4>
100108
</div>
101109
</div>
102110
</div>
111+
112+
<div class="modal" id="monthlyReportModal">
113+
<div class="modal-dialog">
114+
<div class="modal-content">
115+
<div class="modal-header">
116+
<button type="button" class="close" data-dismiss="modal">x</button>
117+
<h3>Are you sure you want to run monthly report for this institution?</h3>
118+
</div>
119+
<div style="text-align: center">
120+
<form id="run-monthly-report-form" method="post" action={% url 'institutions:monthly_report' institution.id %}>
121+
{% csrf_token %}
122+
<label for="monthly_report_date">
123+
Report date:
124+
</label>
125+
<input type="date" name="monthly_report_date" id="monthly_report_date"/>
126+
<input type="text" value="INSTITUTIONAL_SUMMARY" name="monthly_reporter" id="monthly_reporter" hidden/>
127+
</form>
128+
</div>
129+
<div class="modal-footer">
130+
<input class="btn btn-danger" type="submit" form="run-monthly-report-form" value="Confirm" />
131+
<button type="button" class="btn btn-default" data-dismiss="modal">
132+
Cancel
133+
</button>
134+
</div>
135+
</div>
136+
</div>
137+
</div>
103138
{% endblock content %}
104139

105140
{% block bottom_js %}

admin/templates/management/commands.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ <h4><u>Daily Reporters, Go!</u></h4>
9696
</section>
9797
<section>
9898
<h4><u>Monthly Reporters, Go!</u></h4>
99-
<p>Use this management command to run all daily metrics reports.</p>
99+
<p>Use this management command to run all monthly metrics reports.</p>
100100
<form method="post"
101101
action="{% url 'management:monthly_reporters_go'%}">
102102
{% csrf_token %}

admin_tests/institutions/test_views.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import json
22
import pytest
3+
from unittest import mock
34

45
from django.test import RequestFactory
56
from django.contrib.auth.models import Permission
67
from django.contrib.contenttypes.models import ContentType
78
from django.core.exceptions import PermissionDenied
9+
from osf_tests.factories import faker, FakeList
810

911
from tests.base import AdminTestCase
1012
from osf_tests.factories import (
@@ -199,6 +201,32 @@ def test_get_view(self):
199201
res = self.view.get(self.request)
200202
assert res.status_code == 200
201203

204+
@mock.patch('admin.institutions.views.monthly_reporter_do.apply_async')
205+
def test_monthly_reporter_called_on_create(self, mock_monthly_reporter_do):
206+
data = {
207+
'_id': 'wqhx1',
208+
'name': 'company',
209+
'login_url': faker.url(),
210+
'logout_url': faker.url(),
211+
'identifier_domain': faker.url(),
212+
'ror_uri': faker.url(),
213+
'domains': FakeList('url', n=3),
214+
'email_domains': FakeList('domain_name', n=1),
215+
'orcid_record_verified_source': '',
216+
'delegation_protocol': '',
217+
'institutional_request_access_enabled': False
218+
}
219+
form = InstitutionForm(data=data)
220+
assert form.is_valid()
221+
222+
view = setup_form_view(self.base_view(), self.request, form=form)
223+
view.object = form.save()
224+
view.form_valid(form)
225+
226+
mock_monthly_reporter_do.assert_called_once()
227+
_, kwargs = mock_monthly_reporter_do.call_args
228+
assert kwargs['kwargs']['report_kwargs']['institution_pk'] == view.object.id
229+
202230

203231
class TestAffiliatedNodeList(AdminTestCase):
204232
def setUp(self):

api/institutions/views.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,11 @@ def get_object(self):
624624
return object
625625

626626
def get_default_search(self):
627-
yearmonth = InstitutionMonthlySummaryReport.most_recent_yearmonth()
627+
base_search = InstitutionMonthlySummaryReport.search().filter(
628+
'term',
629+
institution_id=self.get_institution()._id,
630+
)
631+
yearmonth = InstitutionMonthlySummaryReport.most_recent_yearmonth(base_search=base_search)
628632
if report_date_str := self.request.query_params.get('report_yearmonth'):
629633
try:
630634
yearmonth = YearMonth.from_str(report_date_str)
@@ -634,12 +638,9 @@ def get_default_search(self):
634638
if yearmonth is None:
635639
return None
636640

637-
return InstitutionMonthlySummaryReport.search().filter(
641+
return base_search.filter(
638642
'term',
639643
report_yearmonth=str(yearmonth),
640-
).filter(
641-
'term',
642-
institution_id=self.get_institution()._id,
643644
)
644645

645646

osf/management/commands/monthly_reporters_go.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,12 @@ def schedule_monthly_reporter(
7979
retry_backoff=True,
8080
)
8181
def monthly_reporter_do(reporter_key: str, yearmonth: str, report_kwargs: dict):
82-
_reporter = _get_reporter(reporter_key, yearmonth)
82+
try:
83+
_reporter = _get_reporter(reporter_key, yearmonth)
84+
except KeyError as exc:
85+
framework.sentry.log_exception(exc)
86+
return
87+
8388
_report = _reporter.report(**report_kwargs)
8489
if _report is not None:
8590
_report.report_yearmonth = _reporter.yearmonth

osf/metrics/reports.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class Meta:
9090
@classmethod
9191
def most_recent_yearmonth(cls, base_search=None) -> YearMonth | None:
9292
_search = base_search or cls.search()
93-
_search = _search.update_from_dict({'size': 0}) # omit hits
93+
_search = _search[0:0] # omit hits
9494
_search.aggs.bucket(
9595
'agg_most_recent_yearmonth',
9696
'terms',
@@ -101,8 +101,12 @@ def most_recent_yearmonth(cls, base_search=None) -> YearMonth | None:
101101
_response = _search.execute()
102102
if not _response.aggregations:
103103
return None
104-
(_bucket,) = _response.aggregations.agg_most_recent_yearmonth.buckets
105-
return _bucket.key
104+
105+
buckets = _response.aggregations.agg_most_recent_yearmonth.buckets
106+
if not buckets:
107+
return None
108+
109+
return buckets[0].key
106110

107111
def __init_subclass__(cls, **kwargs):
108112
super().__init_subclass__(**kwargs)

0 commit comments

Comments
 (0)