diff --git a/api/base/utils.py b/api/base/utils.py index 9e0dcbc7e8c7..d049216b8d70 100644 --- a/api/base/utils.py +++ b/api/base/utils.py @@ -2,6 +2,7 @@ from urllib.parse import urlunsplit, urlsplit, parse_qs, urlencode from packaging.version import Version from hashids import Hashids +import waffle from django.apps import apps from django.core.exceptions import ObjectDoesNotExist @@ -275,3 +276,17 @@ def __len__(self): def add_dict_as_item(self, dict): item = type('item', (object,), dict) self.append(item) + + +def view_toggled_by_feature_flag(flag_name, view_name, old_view, new_view): + '''return a view function that, depending on a feature flag, uses "old" or "new" view behavior + + (when removing that feature flag: delete this view, delete the "old" view, + and rename the "new" view to replace this one (and not say "new" anymore)) + ''' + def _waffled_view(request, *args, **kwargs): + if waffle.flag_is_active(request, flag_name): + return new_view(request, *args, **kwargs) + return old_view(request, *args, **kwargs) + _waffled_view.view_name = view_name + return _waffled_view diff --git a/api/institutions/serializers.py b/api/institutions/serializers.py index 54e25d4d263f..7d84f7345429 100644 --- a/api/institutions/serializers.py +++ b/api/institutions/serializers.py @@ -270,7 +270,7 @@ def get_absolute_url(self, obj): ) -class InstitutionUserMetricsSerializer(JSONAPISerializer): +class OldInstitutionUserMetricsSerializer(JSONAPISerializer): class Meta: type_ = 'institution-users' @@ -306,3 +306,11 @@ def get_absolute_url(self, obj): 'version': 'v2', }, ) + + +class NewInstitutionUserMetricsSerializer(JSONAPISerializer): + + class Meta: + type_ = 'institution-users' + + ... # TODO: serializer fields diff --git a/api/institutions/urls.py b/api/institutions/urls.py index be4f9ca0b43d..920da3581182 100644 --- a/api/institutions/urls.py +++ b/api/institutions/urls.py @@ -15,5 +15,5 @@ re_path(r'^(?P\w+)/users/$', views.InstitutionUserList.as_view(), name=views.InstitutionUserList.view_name), re_path(r'^(?P\w+)/metrics/summary/$', views.InstitutionSummaryMetrics.as_view(), name=views.InstitutionSummaryMetrics.view_name), re_path(r'^(?P\w+)/metrics/departments/$', views.InstitutionDepartmentList.as_view(), name=views.InstitutionDepartmentList.view_name), - re_path(r'^(?P\w+)/metrics/users/$', views.InstitutionUserMetricsList.as_view(), name=views.InstitutionUserMetricsList.view_name), + re_path(r'^(?P\w+)/metrics/users/$', views.institution_user_metrics_view, name=views.institution_user_metrics_view.view_name), ] diff --git a/api/institutions/views.py b/api/institutions/views.py index d21c15e07465..8e014637b5f4 100644 --- a/api/institutions/views.py +++ b/api/institutions/views.py @@ -8,13 +8,14 @@ from framework.auth.oauth_scopes import CoreScopes +import osf.features from osf.metrics import InstitutionProjectCounts from osf.models import OSFUser, Node, Institution, Registration from osf.metrics import UserInstitutionProjectCounts from osf.utils import permissions as osf_permissions from api.base import permissions as base_permissions -from api.base.filters import ListFilterMixin +from api.base.filters import ListFilterMixin, FilterMixin from api.base.views import JSONAPIBaseView from api.base.serializers import JSONAPISerializer from api.base.utils import get_object_or_error, get_user_auth @@ -25,7 +26,10 @@ ) from api.base.settings import MAX_SIZE_OF_ES_QUERY from api.base.exceptions import RelationshipPostMakesNoChanges -from api.base.utils import MockQueryset +from api.base.utils import ( + MockQueryset, + view_toggled_by_feature_flag, +) from api.base.settings import DEFAULT_ES_NULL_VALUE from api.metrics.permissions import IsInstitutionalMetricsUser from api.nodes.serializers import NodeSerializer @@ -40,7 +44,8 @@ InstitutionRegistrationsRelationshipSerializer, InstitutionSummaryMetricSerializer, InstitutionDepartmentMetricsSerializer, - InstitutionUserMetricsSerializer, + NewInstitutionUserMetricsSerializer, + OldInstitutionUserMetricsSerializer, ) from api.institutions.permissions import UserIsAffiliated from api.institutions.renderers import InstitutionDepartmentMetricsCSVRenderer, InstitutionUserMetricsCSVRenderer, MetricsCSVRenderer @@ -493,10 +498,10 @@ def get_default_queryset(self): return self._make_elasticsearch_results_filterable(search, id=institution._id) -class InstitutionUserMetricsList(InstitutionImpactList): +class _OldInstitutionUserMetricsList(InstitutionImpactList): view_name = 'institution-user-metrics' - serializer_class = InstitutionUserMetricsSerializer + serializer_class = OldInstitutionUserMetricsSerializer renderer_classes = tuple(api_settings.DEFAULT_RENDERER_CLASSES) + (InstitutionUserMetricsCSVRenderer,) ordering_fields = ('user_name', 'department') @@ -521,3 +526,29 @@ def get_default_queryset(self): institution = self.get_institution() search = UserInstitutionProjectCounts.get_current_user_metrics(institution) return self._make_elasticsearch_results_filterable(search, id=institution._id, department=DEFAULT_ES_NULL_VALUE) + + +class _NewInstitutionUserMetricsList(JSONAPIBaseView, FilterMixin, generics.ListAPIView): + 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-user-metrics' + + serializer_class = NewInstitutionUserMetricsSerializer + + ... # TODO: view logic + + +institution_user_metrics_view = view_toggled_by_feature_flag( + flag_name=osf.features.INSTITUTIONAL_DASHBOARD_2024, + view_name='institution-user-metrics', + old_view=_OldInstitutionUserMetricsList.as_view(), + new_view=_NewInstitutionUserMetricsList.as_view(), +) diff --git a/osf/features.yaml b/osf/features.yaml index c6f02ce29941..a3f0fcc1f14e 100644 --- a/osf/features.yaml +++ b/osf/features.yaml @@ -189,6 +189,10 @@ flags: note: This is not used everyone: true + - flag_name: INSTITUTIONAL_DASHBOARD_2024 + name: institutional_dashboard_2024 + note: whether to surface older or updated (in 2024) institutional metrics + switches: - flag_name: DISABLE_ENGAGEMENT_EMAILS name: disable_engagement_emails