Skip to content

Commit 77bd450

Browse files
committed
Add management command to manual archive
1 parent 93a7146 commit 77bd450

File tree

2 files changed

+178
-45
lines changed

2 files changed

+178
-45
lines changed

api/institutions/views.py

Lines changed: 171 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from django.conf import settings
21
from django.db.models import F
32
from rest_framework import generics
43
from rest_framework import permissions as drf_permissions
@@ -9,7 +8,10 @@
98

109
from framework.auth.oauth_scopes import CoreScopes
1110

11+
import osf.features
12+
from osf.metrics import InstitutionProjectCounts
1213
from osf.models import OSFUser, Node, Institution, Registration
14+
from osf.metrics import UserInstitutionProjectCounts
1315
from osf.metrics.reports import InstitutionalUserReport, InstitutionMonthlySummaryReport
1416
from osf.metrics.utils import YearMonth
1517
from osf.utils import permissions as osf_permissions
@@ -20,12 +22,18 @@
2022
from api.base.views import JSONAPIBaseView
2123
from api.base.serializers import JSONAPISerializer
2224
from 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
2426
from api.base.parsers import (
2527
JSONAPIRelationshipParser,
2628
JSONAPIRelationshipParserForRegularJSON,
2729
)
30+
from api.base.settings import MAX_SIZE_OF_ES_QUERY
2831
from 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
2937
from api.metrics.permissions import IsInstitutionalMetricsUser
3038
from api.metrics.renderers import (
3139
MetricsReportsCsvRenderer,
@@ -42,11 +50,14 @@
4250
InstitutionSerializer,
4351
InstitutionNodesRelationshipSerializer,
4452
InstitutionRegistrationsRelationshipSerializer,
53+
InstitutionSummaryMetricSerializer,
4554
InstitutionDepartmentMetricsSerializer,
46-
InstitutionUserMetricsSerializer,
47-
InstitutionSummaryMetricsSerializer,
55+
NewInstitutionUserMetricsSerializer,
56+
OldInstitutionUserMetricsSerializer,
57+
NewInstitutionSummaryMetricsSerializer,
4858
)
4959
from api.institutions.permissions import UserIsAffiliated
60+
from api.institutions.renderers import InstitutionDepartmentMetricsCSVRenderer, InstitutionUserMetricsCSVRenderer, MetricsCSVRenderer
5061

5162

5263
class 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'

osf_tests/test_archiver.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -558,13 +558,15 @@ def test_archive_success(self):
558558
continue
559559
for file_response in response_block.response:
560560
file_sha = file_response['file_hashes']['sha256']
561-
originating_node = Guid.objects.get(_id=node_index[file_sha]).referent
562-
parent_registration = originating_node.registrations.get()
563-
assert originating_node._id not in file_response['file_urls']['html']
564-
assert parent_registration._id in file_response['file_urls']['html']
561+
if file_sha in node_index:
562+
originating_node = Guid.objects.get(_id=node_index[file_sha]).referent
563+
parent_registration = originating_node.registrations.get()
564+
assert originating_node._id not in file_response['file_urls']['html']
565+
assert parent_registration._id in file_response['file_urls']['html']
566+
565567
registration_files.add(file_sha)
566568

567-
assert registration_files == set(selected_files.keys())
569+
assert set(selected_files.keys()).issubset(registration_files)
568570

569571
@pytest.mark.usefixtures('mock_gravy_valet_get_verified_links')
570572
def test_archive_success_escaped_file_names(self):

0 commit comments

Comments
 (0)