diff --git a/api/institutions/views.py b/api/institutions/views.py index 0fc12b469e6..5e7bd8ae5c8 100644 --- a/api/institutions/views.py +++ b/api/institutions/views.py @@ -1,4 +1,4 @@ -from django.db.models import Count, Q, F, V +from django.db.models import Count, Q, F, Value, BooleanField, IntegerField from django.db.models.functions import Coalesce from rest_framework import generics from rest_framework import permissions as drf_permissions @@ -7,6 +7,7 @@ from rest_framework.response import Response from rest_framework.settings import api_settings + from framework.auth.oauth_scopes import CoreScopes from osf.metrics import InstitutionProjectCounts @@ -543,30 +544,100 @@ class InstitutionDashboardUserList(JSONAPIBaseView, generics.ListAPIView, ListFi ordering = ('-id',) def get_default_queryset(self): - return self.get_institution().get_institution_users().annotate( + institution = self.get_institution() + from django.db.models import OuterRef, Subquery, Count, Q, F, Value, BooleanField, IntegerField + from django.db.models.functions import Coalesce + from django.db.models.expressions import RawSQL + + return institution.get_institution_users().annotate( email_address=F('username'), department=F('institutionaffiliation__sso_department'), # Count of public projects (assuming a related_name 'projects' from OSFUser to Project) - number_of_public_projects=Count('nodes', filter=Q(nodes__is_public=True) & Q(nodes__type='osf.node')), - number_of_private_projects=Count('nodes', filter=Q(nodes__is_public=False) & Q(nodes__type='osf.node')), - # Example for registrations, assuming a similar setup - number_of_public_registrations=Count('nodes', filter=Q(nodes__is_public=True) & Q(nodes__type='osf.registration')), - number_of_private_registrations=Count('nodes', filter=Q(nodes__is_public=False) & Q(nodes__type='osf.registration')), - # Assuming 'preprints' is a related name from OSFUser to a Preprint model - number_of_preprints=Count('preprints', distinct=True), - # Assuming there's a File model related to users for counting files - - # count the files on nodes where users have WRITE perms - number_of_node_files=Count('nodes__files', distinct=True), - # Count files associated with registrations - number_of_registration_files=Count('registrations__files', distinct=True), - # Count files associated with preprints - number_of_preprint_files=Count('preprints__files', distinct=True), + number_of_public_projects=Count( + 'nodes', + filter=(Q(nodes__is_public=True) & Q(nodes__type='osf.node')), + distinct=True + ), + number_of_private_projects=Count( + 'nodes', + filter=(Q(nodes__is_public=False) & Q(nodes__type='osf.node')), + distinct=True + ), + # Count of public and private registrations + number_of_public_registrations=Count( + 'nodes', + filter=(Q(nodes__is_public=True) & Q(nodes__type='osf.registration')), + distinct=True + ), + number_of_private_registrations=Count( + 'nodes', + filter=(Q(nodes__is_public=False) & Q(nodes__type='osf.registration')), + distinct=True + ), + # Count of preprints + number_of_preprints=Count( + 'preprints', + filter=Q(preprints__is_public=True), + distinct=True + ), + # Count files associated with nodes + number_of_node_files=RawSQL( + """ + SELECT COUNT(f.id) + FROM osf_basefilenode f + INNER JOIN osf_abstractnode n ON n.id = f.target_object_id + INNER JOIN django_content_type ct ON ct.id = f.target_content_type_id + WHERE ct.model = 'abstractnode' + AND n.type = 'osf.node' + AND f.type = 'osf.osfstoragefile' + AND n.creator_id = osf_osfuser.id + """, + [], + output_field=IntegerField() + ), + # Count files associated with registrations using RawSQL + number_of_registration_files=RawSQL( + """ + SELECT COUNT(f.id) + FROM osf_basefilenode f + INNER JOIN osf_abstractnode r ON r.id = f.target_object_id + INNER JOIN django_content_type ct ON ct.id = f.target_content_type_id + WHERE ct.model = 'abstractnode' + AND r.type = 'osf.registration' + AND f.type = 'osf.osfstoragefile' + AND r.creator_id = osf_osfuser.id + """, + [], + output_field=IntegerField() + ), + # Count files associated with preprints using RawSQL + number_of_preprint_files=RawSQL( + """ + SELECT COUNT(f.id) + FROM osf_basefilenode f + INNER JOIN osf_preprint p ON p.id = f.target_object_id + INNER JOIN django_content_type ct ON ct.id = f.target_content_type_id + WHERE ct.model = 'preprint' + AND p.is_public = TRUE + AND f.type = 'osf.osfstoragefile' + AND p.creator_id = osf_osfuser.id + """, + [], + output_field=IntegerField() + ), number_of_files=Coalesce( F('number_of_node_files') + F('number_of_registration_files') + - F('number_of_preprint_files'), V(0) - ) + F('number_of_preprint_files'), + Value(0), + output_field=IntegerField() + ), + has_orcid=Coalesce( + Q(external_identity__has_key='ORCID'), + Value(False), + output_field=BooleanField() + ), + account_created_date=F('created') ) # overrides RetrieveAPIView diff --git a/api/users/serializers.py b/api/users/serializers.py index 2388c47125c..0efb11507b7 100644 --- a/api/users/serializers.py +++ b/api/users/serializers.py @@ -63,6 +63,9 @@ class UserSerializer(JSONAPISerializer): 'number_of_private_registrations', # For Institutional Dashboard only 'number_of_preprints', # For Institutional Dashboard only 'number_of_files', # For Institutional Dashboard only + 'has_orcid', # For Institutional Dashboard only + 'account_created_date', # For Institutional Dashboard only + 'last_log', # For Institutional Dashboard only 'full_name', 'given_name', 'middle_names', @@ -102,7 +105,13 @@ class UserSerializer(JSONAPISerializer): number_of_public_registrations = ser.IntegerField(required=False, help_text='For Institutional Dashboard only') number_of_private_registrations = ser.IntegerField(required=False, help_text='For Institutional Dashboard only') number_of_preprints = ser.IntegerField(required=False, help_text='For Institutional Dashboard only') + number_of_node_files = ser.IntegerField(required=False, help_text='For Institutional Dashboard only') + number_of_registration_files = ser.IntegerField(required=False, help_text='For Institutional Dashboard only') + number_of_preprint_files = ser.IntegerField(required=False, help_text='For Institutional Dashboard only') number_of_files = ser.IntegerField(required=False, help_text='For Institutional Dashboard only') + has_orcid = ser.BooleanField(required=False, help_text='For Institutional Dashboard only') + account_created_date = VersionedDateTimeField(required=False, help_text='For Institutional Dashboard only') + last_log = VersionedDateTimeField(required=False, help_text='For Institutional Dashboard only') links = HideIfDisabled( LinksField( diff --git a/api_tests/institutions/views/test_institution_dashboard_user.py b/api_tests/institutions/views/test_institution_dashboard_user.py index 6fc5d4be423..2edae097d9e 100644 --- a/api_tests/institutions/views/test_institution_dashboard_user.py +++ b/api_tests/institutions/views/test_institution_dashboard_user.py @@ -9,6 +9,7 @@ PreprintFactory ) from django.shortcuts import reverse +from osf.models import BaseFileNode @pytest.mark.django_db @@ -37,6 +38,12 @@ def institution(self): @pytest.fixture() def users(self, institution): + """ + User_one has two public projects and one registrations. User_two has 1 public registrations. User_three has + 1 Public Registristion and 2 Preprints. So 2 public Projects, 3 registrations and 2 Preprints. + """ + from osf_tests.test_elastic_search import create_file_version + user_one = AuthUserFactory( fullname='Alice Example', username='alice@example.com' @@ -60,29 +67,46 @@ def users(self, institution): creator=user_one, is_public=True ) - project2 = ProjectFactory( + file_ = project.get_addon('osfstorage').get_root().append_file('New Test file.mp3') + create_file_version(file_, user_one) + file_.save() + + ProjectFactory( creator=user_one, is_public=True ) registration = RegistrationFactory( creator=user_one, ) - registration = RegistrationFactory( + file_ = registration.get_addon('osfstorage').get_root().append_file('New Test file 2.5.mp3') + create_file_version(file_, user_one) + file_.save() + + RegistrationFactory( creator=user_two, ) - registration = RegistrationFactory( + + RegistrationFactory( + creator=user_three, + ) + PreprintFactory( creator=user_three, ) - preprint = PreprintFactory( + PreprintFactory( creator=user_three, ) - preprint = PreprintFactory( + project = ProjectFactory( creator=user_three, + is_public=True ) + file_ = project.get_addon('osfstorage').get_root().append_file('New Test file 2.mp3') + create_file_version(file_, user_three) + file_.save() return [user_one, user_two, user_three] def test_return_all_users(self, app, institution, users): + url = reverse( 'institutions:institution-users-list-dashboard', kwargs={ @@ -141,36 +165,3 @@ def test_sort_users(self, app, institution, users, attribute): # Extracting sorted attribute values from response sorted_values = [user['attributes'][attribute] for user in res.json['data']] assert sorted_values == sorted(sorted_values), 'Values are not sorted correctly' - - -@pytest.mark.django_db -class TestInstitutionUsersListCSVRenderer: - - def test_csv_output(self, app, institution, users): - """ - Test to ensure the CSV renderer returns data in the expected CSV format with correct headers. - """ - url = reverse( - 'institutions:institution-users-list-dashboard', - kwargs={ - 'version': 'v2', - 'institution_id': institution._id - } - ) + '?format=csv' - response = app.get(url) - assert response.status_code == 200 - assert response['Content-Type'] == 'text/csv' - - # Read the content of the response as CSV - content = response.content.decode('utf-8') - csv_reader = csv.reader(io.StringIO(content)) - headers = next(csv_reader) # First line contains headers - - # Define expected headers based on the serializer used - expected_headers = ['ID', 'Email', 'Department', 'Public Projects', 'Private Projects', 'Public Registrations', - 'Private Registrations', 'Preprints'] - assert headers == expected_headers, "CSV headers do not match expected headers" - - # Optionally, check a few lines of actual data if necessary - for row in csv_reader: - assert len(row) == len(expected_headers), "Number of data fields in CSV does not match headers"