1- from django .conf import settings
21from django .db .models import F
32from rest_framework import generics
43from rest_framework import permissions as drf_permissions
98
109from framework .auth .oauth_scopes import CoreScopes
1110
11+ import osf .features
12+ from osf .metrics import InstitutionProjectCounts
1213from osf .models import OSFUser , Node , Institution , Registration
14+ from osf .metrics import UserInstitutionProjectCounts
1315from osf .metrics .reports import InstitutionalUserReport , InstitutionMonthlySummaryReport
1416from osf .metrics .utils import YearMonth
1517from osf .utils import permissions as osf_permissions
2022from api .base .views import JSONAPIBaseView
2123from api .base .serializers import JSONAPISerializer
2224from api .base .utils import get_object_or_error , get_user_auth
23- from api .base .pagination import MaxSizePagination , JSONAPINoPagination
25+ from api .base .pagination import JSONAPIPagination , MaxSizePagination
2426from api .base .parsers import (
2527 JSONAPIRelationshipParser ,
2628 JSONAPIRelationshipParserForRegularJSON ,
2729)
30+ from api .base .settings import MAX_SIZE_OF_ES_QUERY
2831from api .base .exceptions import RelationshipPostMakesNoChanges
32+ from api .base .utils import (
33+ MockQueryset ,
34+ toggle_view_by_flag ,
35+ )
36+ from api .base .settings import DEFAULT_ES_NULL_VALUE
2937from api .metrics .permissions import IsInstitutionalMetricsUser
3038from api .metrics .renderers import (
3139 MetricsReportsCsvRenderer ,
4250 InstitutionSerializer ,
4351 InstitutionNodesRelationshipSerializer ,
4452 InstitutionRegistrationsRelationshipSerializer ,
53+ InstitutionSummaryMetricSerializer ,
4554 InstitutionDepartmentMetricsSerializer ,
46- InstitutionUserMetricsSerializer ,
47- InstitutionSummaryMetricsSerializer ,
55+ NewInstitutionUserMetricsSerializer ,
56+ OldInstitutionUserMetricsSerializer ,
57+ NewInstitutionSummaryMetricsSerializer ,
4858)
4959from api .institutions .permissions import UserIsAffiliated
60+ from api .institutions .renderers import InstitutionDepartmentMetricsCSVRenderer , InstitutionUserMetricsCSVRenderer , MetricsCSVRenderer
5061
5162
5263class InstitutionMixin :
@@ -387,52 +398,100 @@ def create(self, *args, **kwargs):
387398 return ret
388399
389400
390- class InstitutionDepartmentList (InstitutionMixin , ElasticsearchListView ):
401+ class _OldInstitutionSummaryMetrics (JSONAPIBaseView , generics .RetrieveAPIView , InstitutionMixin ):
402+ permission_classes = (
403+ drf_permissions .IsAuthenticatedOrReadOnly ,
404+ base_permissions .TokenHasScope ,
405+ IsInstitutionalMetricsUser ,
406+ )
407+
408+ required_read_scopes = [CoreScopes .INSTITUTION_METRICS_READ ]
409+ required_write_scopes = [CoreScopes .NULL ]
410+
391411 view_category = 'institutions'
392- view_name = 'institution-department-metrics'
412+ view_name = 'institution-summary-metrics'
413+
414+ serializer_class = InstitutionSummaryMetricSerializer
415+
416+ # overrides RetrieveAPIView
417+ def get_object (self ):
418+ institution = self .get_institution ()
419+ return InstitutionProjectCounts .get_latest_institution_project_document (institution )
420+
393421
422+ class InstitutionImpactList (JSONAPIBaseView , ListFilterMixin , generics .ListAPIView , InstitutionMixin ):
394423 permission_classes = (
395424 drf_permissions .IsAuthenticatedOrReadOnly ,
396425 base_permissions .TokenHasScope ,
397426 IsInstitutionalMetricsUser ,
398427 )
428+
399429 required_read_scopes = [CoreScopes .INSTITUTION_METRICS_READ ]
400430 required_write_scopes = [CoreScopes .NULL ]
401431
432+ view_category = 'institutions'
433+
434+ @property
435+ def is_csv_export (self ):
436+ if isinstance (self .request .accepted_renderer , MetricsCSVRenderer ):
437+ return True
438+ return False
439+
440+ @property
441+ def pagination_class (self ):
442+ if self .is_csv_export :
443+ return MaxSizePagination
444+ return JSONAPIPagination
445+
446+ def _format_search (self , search , default_kwargs = None ):
447+ raise NotImplementedError ()
448+
449+ def _paginate (self , search ):
450+ if self .pagination_class is MaxSizePagination :
451+ return search .extra (size = MAX_SIZE_OF_ES_QUERY )
452+
453+ page = self .request .query_params .get ('page' )
454+ page_size = self .request .query_params .get ('page[size]' )
455+
456+ if page_size :
457+ page_size = int (page_size )
458+ else :
459+ page_size = api_settings .PAGE_SIZE
460+
461+ if page :
462+ search = search .extra (size = int (page ) * page_size )
463+ return search
464+
465+ def _make_elasticsearch_results_filterable (self , search , ** kwargs ) -> MockQueryset :
466+ """
467+ Since ES returns a list obj instead of a awesome filterable queryset we are faking the filter feature used by
468+ querysets by create a mock queryset with limited filterbility.
469+
470+ :param departments: Dict {'Department Name': 3} means "Department Name" has 3 users.
471+ :return: mock_queryset
472+ """
473+ items = self ._format_search (search , default_kwargs = kwargs )
474+
475+ search = self ._paginate (search )
476+
477+ queryset = MockQueryset (items , search )
478+ return queryset
479+
480+ # overrides RetrieveApiView
481+ def get_queryset (self ):
482+ return self .get_queryset_from_request ()
483+
484+
485+ class InstitutionDepartmentList (InstitutionImpactList ):
486+ view_name = 'institution-department-metrics'
487+
402488 serializer_class = InstitutionDepartmentMetricsSerializer
403- renderer_classes = (
404- * api_settings .DEFAULT_RENDERER_CLASSES ,
405- MetricsReportsCsvRenderer ,
406- MetricsReportsTsvRenderer ,
407- MetricsReportsJsonRenderer ,
408- )
409- pagination_class = JSONAPINoPagination
489+ renderer_classes = tuple (api_settings .DEFAULT_RENDERER_CLASSES ) + (InstitutionDepartmentMetricsCSVRenderer ,)
410490
411- def get_default_search (self ):
412- _base_search = (
413- InstitutionalUserReport .search ()
414- .filter ('term' , institution_id = self .get_institution ()._id )
415- )
416- _yearmonth = InstitutionalUserReport .most_recent_yearmonth (base_search = _base_search )
417- if _yearmonth is None :
418- return None
419- _search = (
420- _base_search
421- .filter ('term' , report_yearmonth = str (_yearmonth ))
422- .exclude ('term' , user_name = 'Deleted user' )
423- )
424- # add aggregation on department name
425- _search .aggs .bucket (
426- 'agg_departments' ,
427- 'terms' ,
428- field = 'department_name' ,
429- missing = settings .DEFAULT_ES_NULL_VALUE ,
430- size = settings .MAX_SIZE_OF_ES_QUERY ,
431- )
432- return _search
491+ ordering_fields = ('-number_of_users' , 'name' )
492+ ordering = ('-number_of_users' , 'name' )
433493
434494 def get_queryset (self ):
435- # execute the search and return a list from the aggregation on department name
436495 _search = super ().get_queryset ()
437496 if not _search :
438497 return []
@@ -442,9 +501,62 @@ def get_queryset(self):
442501 for _bucket in _results .aggregations ['agg_departments' ].buckets
443502 ]
444503
504+ def _format_search (self , search , default_kwargs = None ):
505+ results = search .execute ()
506+
507+ if results .aggregations :
508+ buckets = results .aggregations ['date_range' ]['departments' ].buckets
509+ department_data = [{'name' : bucket ['key' ], 'number_of_users' : len (bucket ['users' ]['buckets' ])} for bucket in buckets ]
510+ return department_data
511+ else :
512+ return []
513+
514+ def get_default_queryset (self ):
515+ institution = self .get_institution ()
516+ search = UserInstitutionProjectCounts .get_department_counts (institution )
517+ return self ._make_elasticsearch_results_filterable (search , id = institution ._id )
518+
445519
446- class InstitutionUserMetricsList ( InstitutionMixin , ElasticsearchListView ):
520+ class _OldInstitutionUserMetricsList ( InstitutionImpactList ):
447521 '''list view for institution-users metrics
522+
523+ used only when the INSTITUTIONAL_DASHBOARD_2024 feature flag is NOT active
524+ (and should be removed when that flag is permanently active)
525+ '''
526+ view_name = 'institution-user-metrics'
527+
528+ serializer_class = OldInstitutionUserMetricsSerializer
529+ renderer_classes = tuple (api_settings .DEFAULT_RENDERER_CLASSES ) + (InstitutionUserMetricsCSVRenderer ,)
530+
531+ ordering_fields = ('user_name' , 'department' )
532+ ordering = ('user_name' ,)
533+
534+ def _format_search (self , search , default_kwargs = None ):
535+ results = search .execute ()
536+
537+ users = []
538+ for user_record in results :
539+ record_dict = {}
540+ record_dict .update (default_kwargs )
541+ record_dict .update (user_record .to_dict ())
542+ user_id = user_record .user_id
543+ fullname = OSFUser .objects .get (guids___id = user_id ).fullname
544+ record_dict ['user_name' ] = fullname
545+ users .append (record_dict )
546+
547+ return users
548+
549+ def get_default_queryset (self ):
550+ institution = self .get_institution ()
551+ search = UserInstitutionProjectCounts .get_current_user_metrics (institution )
552+ return self ._make_elasticsearch_results_filterable (search , id = institution ._id , department = DEFAULT_ES_NULL_VALUE )
553+
554+
555+ class _NewInstitutionUserMetricsList (InstitutionMixin , ElasticsearchListView ):
556+ '''list view for institution-users metrics
557+
558+ used only when the INSTITUTIONAL_DASHBOARD_2024 feature flag is active
559+ (and should be renamed without "New" when that flag is permanently active)
448560 '''
449561 permission_classes = (
450562 drf_permissions .IsAuthenticatedOrReadOnly ,
@@ -464,7 +576,7 @@ class InstitutionUserMetricsList(InstitutionMixin, ElasticsearchListView):
464576 MetricsReportsJsonRenderer ,
465577 )
466578
467- serializer_class = InstitutionUserMetricsSerializer
579+ serializer_class = NewInstitutionUserMetricsSerializer
468580
469581 default_ordering = '-storage_byte_count'
470582 ordering_fields = frozenset ((
@@ -498,8 +610,11 @@ def get_default_search(self):
498610 )
499611
500612
501- class InstitutionSummaryMetricsDetail (JSONAPIBaseView , generics .RetrieveAPIView , InstitutionMixin ):
613+ class _NewInstitutionSummaryMetricsDetail (JSONAPIBaseView , generics .RetrieveAPIView , InstitutionMixin ):
502614 '''detail view for institution-summary metrics
615+
616+ used only when the INSTITUTIONAL_DASHBOARD_2024 feature flag is active
617+ (and should be renamed without "New" when that flag is permanently active)
503618 '''
504619 permission_classes = (
505620 drf_permissions .IsAuthenticatedOrReadOnly ,
@@ -513,7 +628,7 @@ class InstitutionSummaryMetricsDetail(JSONAPIBaseView, generics.RetrieveAPIView,
513628 view_category = 'institutions'
514629 view_name = 'institution-summary-metrics'
515630
516- serializer_class = InstitutionSummaryMetricsSerializer
631+ serializer_class = NewInstitutionSummaryMetricsSerializer
517632
518633 def get_object (self ):
519634 institution = self .get_institution ()
@@ -542,3 +657,19 @@ def get_default_search(self):
542657 'term' ,
543658 report_yearmonth = str (yearmonth ),
544659 )
660+
661+
662+ institution_summary_metrics_detail_view = toggle_view_by_flag (
663+ flag_name = osf .features .INSTITUTIONAL_DASHBOARD_2024 ,
664+ old_view = _OldInstitutionSummaryMetrics .as_view (),
665+ new_view = _NewInstitutionSummaryMetricsDetail .as_view (),
666+ )
667+ institution_summary_metrics_detail_view .view_name = 'institution-summary-metrics'
668+
669+
670+ institution_user_metrics_list_view = toggle_view_by_flag (
671+ flag_name = osf .features .INSTITUTIONAL_DASHBOARD_2024 ,
672+ old_view = _OldInstitutionUserMetricsList .as_view (),
673+ new_view = _NewInstitutionUserMetricsList .as_view (),
674+ )
675+ institution_user_metrics_list_view .view_name = 'institution-user-metrics'
0 commit comments