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-6125] Implement Required Changes to Institution-Metric Summary API #10757

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
38 changes: 38 additions & 0 deletions api/institutions/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,44 @@ def get_absolute_url(self):
return None # there is no detail view for institution-users


class NewInstitutionSummaryMetricsSerializer(JSONAPISerializer):
'''serializer for institution-summary metrics

used only when the INSTITUTIONAL_DASHBOARD_2024 feature flag is active
(and should be renamed without "New" when that flag is permanently active)
'''

class Meta:
type_ = 'institution-summary-metrics'

id = IDField(read_only=True)

user_count = ser.IntegerField(read_only=True)
public_project_count = ser.IntegerField(read_only=True)
private_project_count = ser.IntegerField(read_only=True)
public_registration_count = ser.IntegerField(read_only=True)
embargoed_registration_count = ser.IntegerField(read_only=True)
published_preprint_count = ser.IntegerField(read_only=True)
public_file_count = ser.IntegerField(read_only=True)
storage_byte_count = ser.IntegerField(read_only=True)
monthly_logged_in_user_count = ser.IntegerField(read_only=True)
monthly_active_user_count = ser.IntegerField(read_only=True)

user = RelationshipField(
related_view='users:user-detail',
related_view_kwargs={'user_id': '<user_id>'},
)
institution = RelationshipField(
related_view='institutions:institution-detail',
related_view_kwargs={'institution_id': '<institution_id>'},
)

links = LinksField({})

def get_absolute_url(self):
return None # there is no detail view for institution-users


class InstitutionRelated(JSONAPIRelationshipSerializer):
id = ser.CharField(source='_id', required=False, allow_null=True)
class Meta:
Expand Down
2 changes: 1 addition & 1 deletion api/institutions/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
re_path(r'^(?P<institution_id>\w+)/relationships/registrations/$', views.InstitutionRegistrationsRelationship.as_view(), name=views.InstitutionRegistrationsRelationship.view_name),
re_path(r'^(?P<institution_id>\w+)/relationships/nodes/$', views.InstitutionNodesRelationship.as_view(), name=views.InstitutionNodesRelationship.view_name),
re_path(r'^(?P<institution_id>\w+)/users/$', views.InstitutionUserList.as_view(), name=views.InstitutionUserList.view_name),
re_path(r'^(?P<institution_id>\w+)/metrics/summary/$', views.InstitutionSummaryMetrics.as_view(), name=views.InstitutionSummaryMetrics.view_name),
re_path(r'^(?P<institution_id>\w+)/metrics/summary/$', views.institution_summary_metrics_detail_view, name=views.institution_summary_metrics_detail_view.view_name),
re_path(r'^(?P<institution_id>\w+)/metrics/departments/$', views.InstitutionDepartmentList.as_view(), name=views.InstitutionDepartmentList.view_name),
re_path(r'^(?P<institution_id>\w+)/metrics/users/$', views.institution_user_metrics_list_view, name=views.institution_user_metrics_list_view.view_name),
]
52 changes: 50 additions & 2 deletions api/institutions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from osf.metrics import InstitutionProjectCounts
from osf.models import OSFUser, Node, Institution, Registration
from osf.metrics import UserInstitutionProjectCounts
from osf.metrics.reports import InstitutionalUserReport
from osf.metrics.reports import InstitutionalUserReport, InstitutionMonthlySummaryReport
from osf.utils import permissions as osf_permissions

from api.base import permissions as base_permissions
Expand Down Expand Up @@ -48,6 +48,7 @@
InstitutionDepartmentMetricsSerializer,
NewInstitutionUserMetricsSerializer,
OldInstitutionUserMetricsSerializer,
NewInstitutionSummaryMetricsSerializer,
)
from api.institutions.permissions import UserIsAffiliated
from api.institutions.renderers import InstitutionDepartmentMetricsCSVRenderer, InstitutionUserMetricsCSVRenderer, MetricsCSVRenderer
Expand Down Expand Up @@ -391,7 +392,7 @@ def create(self, *args, **kwargs):
return ret


class InstitutionSummaryMetrics(JSONAPIBaseView, generics.RetrieveAPIView, InstitutionMixin):
class _OldInstitutionSummaryMetrics(JSONAPIBaseView, generics.RetrieveAPIView, InstitutionMixin):
permission_classes = (
drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
Expand Down Expand Up @@ -581,6 +582,53 @@ def get_default_search(self):
)


class _NewInstitutionSummaryMetricsDetail(JSONAPIBaseView, generics.RetrieveAPIView, InstitutionMixin):
'''detail view for institution-summary metrics

used only when the INSTITUTIONAL_DASHBOARD_2024 feature flag is active
(and should be renamed without "New" when that flag is permanently active)
'''
permission_classes = (
drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
IsInstitutionalMetricsUser,
)

required_read_scopes = [CoreScopes.INSTITUTION_METRICS_READ]
required_write_scopes = [CoreScopes.NULL]

view_category = 'institutions'
view_name = 'institution-summary-metrics'

serializer_class = NewInstitutionSummaryMetricsSerializer

def get_object(self):
institution = self.get_institution()
search_object = self.get_default_search()
if search_object:
object = search_object.execute()[0]
object.id = institution._id
return object

def get_default_search(self):
_yearmonth = InstitutionMonthlySummaryReport.most_recent_yearmonth()
if _yearmonth is None:
return None
return (
InstitutionMonthlySummaryReport.search()
.filter('term', report_yearmonth=str(_yearmonth))
.filter('term', institution_id=self.get_institution()._id)
)


institution_summary_metrics_detail_view = toggle_view_by_flag(
flag_name=osf.features.INSTITUTIONAL_DASHBOARD_2024,
old_view=_OldInstitutionSummaryMetrics.as_view(),
new_view=_NewInstitutionSummaryMetricsDetail.as_view(),
)
institution_summary_metrics_detail_view.view_name = 'institution-summary-metrics'


institution_user_metrics_list_view = toggle_view_by_flag(
flag_name=osf.features.INSTITUTIONAL_DASHBOARD_2024,
old_view=_OldInstitutionUserMetricsList.as_view(),
Expand Down
186 changes: 184 additions & 2 deletions api_tests/institutions/views/test_institution_summary_metrics.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import pytest
import datetime

from waffle.testutils import override_flag
from osf.metrics import InstitutionProjectCounts

from api.base.settings.defaults import API_BASE
from osf_tests.factories import (
InstitutionFactory,
AuthUserFactory,
InstitutionFactory
)
from osf.metrics import InstitutionProjectCounts
from osf.metrics.reports import InstitutionMonthlySummaryReport
from osf import features


@pytest.mark.es_metrics
Expand Down Expand Up @@ -92,3 +96,181 @@ def test_get(self, app, url, institution, user, admin):
'self': f'http://localhost:8000/v2/institutions/{institution._id}/metrics/summary/'
}
}


@pytest.mark.es_metrics
@pytest.mark.django_db
class TestNewInstitutionSummaryMetricsList:
@pytest.fixture(autouse=True)
def _waffled(self):
with override_flag(features.INSTITUTIONAL_DASHBOARD_2024, active=True):
yield

@pytest.fixture()
def institution(self):
return InstitutionFactory()

@pytest.fixture()
def rando(self):
return AuthUserFactory()

@pytest.fixture()
def institutional_admin(self, institution):
admin_user = AuthUserFactory()
institution.get_group('institutional_admins').user_set.add(admin_user)
return admin_user

@pytest.fixture()
def unshown_reports(self, institution):
# Reports that should not be shown in the results
# Report from another institution
another_institution = InstitutionFactory()
_summary_report_factory('2024-08', another_institution)
# Old report from the same institution
_summary_report_factory('2024-07', institution)
_summary_report_factory('2018-02', institution)

@pytest.fixture()
def reports(self, institution):
return [
_summary_report_factory(
'2024-08', institution,
user_count=100,
public_project_count=50,
private_project_count=25,
public_registration_count=10,
embargoed_registration_count=5,
published_preprint_count=15,
public_file_count=20,
storage_byte_count=5000000000,
monthly_logged_in_user_count=80,
monthly_active_user_count=60,
),
_summary_report_factory(
'2024-08', institution,
user_count=200,
public_project_count=150,
private_project_count=125,
public_registration_count=110,
embargoed_registration_count=105,
published_preprint_count=115,
public_file_count=120,
storage_byte_count=15000000000,
monthly_logged_in_user_count=180,
monthly_active_user_count=160,
),
]

@pytest.fixture()
def url(self, institution):
return f'/{API_BASE}institutions/{institution._id}/metrics/summary/'

def test_anon(self, app, url):
resp = app.get(url, expect_errors=True)
assert resp.status_code == 401

def test_rando(self, app, url, rando):
resp = app.get(url, auth=rando.auth, expect_errors=True)
assert resp.status_code == 403

def test_get_empty(self, app, url, institutional_admin):
resp = app.get(url, auth=institutional_admin.auth)
assert resp.status_code == 200
assert resp.json['meta'] == {'version': '2.0'}

def test_get_report(self, app, url, institutional_admin, institution, reports, unshown_reports):
resp = app.get(url, auth=institutional_admin.auth)
assert resp.status_code == 200

data = resp.json['data']

assert data['id'] == institution._id
assert data['type'] == 'institution-summary-metrics'

attributes = data['attributes']
assert attributes['user_count'] == 200
assert attributes['public_project_count'] == 150
assert attributes['private_project_count'] == 125
assert attributes['public_registration_count'] == 110
assert attributes['embargoed_registration_count'] == 105
assert attributes['published_preprint_count'] == 115
assert attributes['public_file_count'] == 120
assert attributes['storage_byte_count'] == 15000000000
assert attributes['monthly_logged_in_user_count'] == 180
assert attributes['monthly_active_user_count'] == 160

def test_get_report_with_multiple_months_and_institutions(
self, app, url, institutional_admin, institution
):
# Create reports for multiple months and institutions
other_institution = InstitutionFactory()
_summary_report_factory(
'2024-09', institution,
user_count=250,
public_project_count=200,
private_project_count=150,
public_registration_count=120,
embargoed_registration_count=110,
published_preprint_count=130,
public_file_count=140,
storage_byte_count=20000000000,
monthly_logged_in_user_count=220,
monthly_active_user_count=200,
)
_summary_report_factory(
'2024-08', institution,
user_count=200,
public_project_count=150,
private_project_count=125,
public_registration_count=110,
embargoed_registration_count=105,
published_preprint_count=115,
public_file_count=120,
storage_byte_count=15000000000,
monthly_logged_in_user_count=180,
monthly_active_user_count=160,
)
_summary_report_factory(
'2024-09', other_institution,
user_count=300,
public_project_count=250,
private_project_count=200,
public_registration_count=180,
embargoed_registration_count=170,
published_preprint_count=190,
public_file_count=210,
storage_byte_count=25000000000,
monthly_logged_in_user_count=270,
monthly_active_user_count=260,
)

resp = app.get(url, auth=institutional_admin.auth)
assert resp.status_code == 200

data = resp.json['data']

assert data['id'] == institution._id
assert data['type'] == 'institution-summary-metrics'

attributes = data['attributes']

assert attributes['user_count'] == 250
assert attributes['public_project_count'] == 200
assert attributes['private_project_count'] == 150
assert attributes['public_registration_count'] == 120
assert attributes['embargoed_registration_count'] == 110
assert attributes['published_preprint_count'] == 130
assert attributes['public_file_count'] == 140
assert attributes['storage_byte_count'] == 20000000000
assert attributes['monthly_logged_in_user_count'] == 220
assert attributes['monthly_active_user_count'] == 200


def _summary_report_factory(yearmonth, institution, **kwargs):
report = InstitutionMonthlySummaryReport(
report_yearmonth=yearmonth,
institution_id=institution._id,
**kwargs,
)
report.save(refresh=True)
return report
15 changes: 15 additions & 0 deletions osf/metrics/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,18 @@ class InstitutionalUserReport(MonthlyReport):
published_preprint_count = metrics.Integer()
public_file_count = metrics.Long()
storage_byte_count = metrics.Long()


class InstitutionMonthlySummaryReport(MonthlyReport):
UNIQUE_TOGETHER_FIELDS = ('report_yearmonth', 'institution_id', )
institution_id = metrics.Keyword()
user_count = metrics.Integer()
public_project_count = metrics.Integer()
private_project_count = metrics.Integer()
public_registration_count = metrics.Integer()
embargoed_registration_count = metrics.Integer()
published_preprint_count = metrics.Integer()
storage_byte_count = metrics.Long()
public_file_count = metrics.Long()
monthly_logged_in_user_count = metrics.Long()
monthly_active_user_count = metrics.Long()
Loading