Skip to content

Commit

Permalink
update institution summary information with new view for monthly report
Browse files Browse the repository at this point in the history
  • Loading branch information
John Tordoff committed Sep 20, 2024
1 parent 884bdfc commit 8e220fc
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 5 deletions.
47 changes: 47 additions & 0 deletions api/institutions/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,53 @@ 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)
Summary contains counts of
- Total users in the institution
- Total public project count for the institution
- Total private project count for the institution
- Total public registration count for the institution
- Total private registration count for the institution
- Total published preprint count for the institution
'''

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_list_view, name=views.institution_summary_metrics_list_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),
]
59 changes: 57 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,60 @@ def get_default_search(self):
)


class _NewInstitutionSummaryMetricsList(JSONAPIBaseView, generics.RetrieveAPIView, InstitutionMixin):
'''list 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_user_metrics_list_view = toggle_view_by_flag(
flag_name=osf.features.INSTITUTIONAL_DASHBOARD_2024,
old_view=_OldInstitutionUserMetricsList.as_view(),
new_view=_NewInstitutionUserMetricsList.as_view(),
)
institution_user_metrics_list_view.view_name = 'institution-user-metrics'

institution_summary_metrics_list_view = toggle_view_by_flag(
flag_name=osf.features.INSTITUTIONAL_DASHBOARD_2024,
old_view=_OldInstitutionSummaryMetrics.as_view(),
new_view=_NewInstitutionSummaryMetricsList.as_view(),
)
institution_summary_metrics_list_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
120 changes: 118 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,115 @@ 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 _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()

0 comments on commit 8e220fc

Please sign in to comment.