Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions admin_tests/institutions/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ def test_institution_form(self):
'name': 'New Name',
'logo_name': 'awesome_logo.png',
'domains': 'http://kris.biz/, http://www.little.biz/',
'_id': 'newawesomeprov'
'_id': 'newawesomeprov',
'sso_availability': 'Public',
}
form = InstitutionForm(data=new_data)
assert form.is_valid()
Expand Down Expand Up @@ -214,7 +215,8 @@ def test_monthly_reporter_called_on_create(self, mock_monthly_reporter_do):
'email_domains': FakeList('domain_name', n=1),
'orcid_record_verified_source': '',
'delegation_protocol': '',
'institutional_request_access_enabled': False
'institutional_request_access_enabled': False,
'sso_availability': 'Public',
}
form = InstitutionForm(data=data)
assert form.is_valid()
Expand Down
18 changes: 0 additions & 18 deletions osf/migrations/0036_institution_sso_in_progress.py

This file was deleted.

18 changes: 18 additions & 0 deletions osf/migrations/0038_institution_sso_availability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.15 on 2026-03-13 11:11

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('osf', '0037_notification_refactor_post_release'),
]

operations = [
migrations.AddField(
model_name='institution',
name='sso_availability',
field=models.CharField(choices=[('Public', 'PUBLIC'), ('Unavailable', 'UNAVAILABLE'), ('Hidden', 'HIDDEN')], default='Hidden', max_length=15),
),
]
24 changes: 23 additions & 1 deletion osf/models/institution.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ class SsoFilterCriteriaAction(Enum):
CONTAINS = 'contains' # Type 2: SSO releases a multi-value attribute, of which one value matches
IN = 'in' # Type 3: SSO releases a single-value attribute that have multiple valid values

class SSOAvailability(Enum):
"""Defines 3 SSO availability states for institutions.
"""
PUBLIC = 'Public' # Active and has a delegation protocol
Comment thread
cslzchen marked this conversation as resolved.
Outdated
UNAVAILABLE = 'Unavailable' # Does not have a delegation protocol
HIDDEN = 'Hidden' # Inactive and has a delegation protocol
Comment thread
cslzchen marked this conversation as resolved.
Outdated


class InstitutionManager(models.Manager):

Expand Down Expand Up @@ -79,6 +86,13 @@ class Institution(DirtyFieldsMixin, Loggable, ObjectIDMixin, BaseModel, Guardian
default=''
)

# Institution SSO availability
sso_availability = models.CharField(
choices=[(choice.value, choice.name) for choice in SSOAvailability],
max_length=15,
default=SSOAvailability.HIDDEN.value
)

# Default Storage Region
storage_regions = models.ManyToManyField(
'addons_osfstorage.Region',
Expand Down Expand Up @@ -125,7 +139,6 @@ class Institution(DirtyFieldsMixin, Loggable, ObjectIDMixin, BaseModel, Guardian
default='',
help_text='Full URL where institutional admins can access archived metrics reports.',
)
sso_in_progress = models.BooleanField(default=False)

class Meta:
# custom permissions for use in the OSF Admin App
Expand Down Expand Up @@ -238,6 +251,11 @@ def deactivate(self):
"""
if not self.deactivated:
self.deactivated = timezone.now()
if not self.delegation_protocol:
self.sso_availability = SSOAvailability.UNAVAILABLE.value
else:
self.sso_availability = SSOAvailability.HIDDEN.value

self.save()
# Django mangers aren't used when querying on related models. Thus, we can query
# affiliated users and send notification emails after the institution has been deactivated.
Expand All @@ -252,6 +270,10 @@ def reactivate(self):
"""
if self.deactivated:
self.deactivated = None
if not self.delegation_protocol:
self.sso_availability = SSOAvailability.UNAVAILABLE.value
else:
self.sso_availability = SSOAvailability.HIDDEN.value
self.save()
else:
message = f'Action rejected - reactivating an active institution [{self._id}].'
Expand Down
25 changes: 25 additions & 0 deletions osf_tests/test_institution.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ def test_deactivated_institution_in_all_institutions(self):
institution.save()
assert institution in Institution.objects.get_all_institutions()

def test_deactivate_sso_institution(self):
institution = InstitutionFactory()
institution.delegation_protocol = 'saml-shib'
institution.save()
with mock.patch.object(
institution,
'_send_deactivation_email',
return_value=None
) as mock__send_deactivation_email:
institution.deactivate()
assert institution.deactivated is not None
assert mock__send_deactivation_email.called
assert institution.sso_availability == 'Hidden'

def test_deactivate_institution(self):
institution = InstitutionFactory()
with mock.patch.object(
Expand All @@ -138,13 +152,24 @@ def test_deactivate_institution(self):
institution.deactivate()
assert institution.deactivated is not None
assert mock__send_deactivation_email.called
assert institution.sso_availability == 'Unavailable'

def test_reactivate_sso_institution(self):
Comment thread
Ostap-Zherebetskyi marked this conversation as resolved.
institution = InstitutionFactory()
institution.delegation_protocol = 'saml-shib'
institution.deactivated = timezone.now()
institution.save()
institution.reactivate()
assert institution.deactivated is None
assert institution.sso_availability == 'Hidden'

def test_reactivate_institution(self):
institution = InstitutionFactory()
institution.deactivated = timezone.now()
institution.save()
institution.reactivate()
assert institution.deactivated is None
assert institution.sso_availability == 'Unavailable'

def test_send_deactivation_email_call_count(self):
institution = InstitutionFactory()
Expand Down
Loading