From 76c909479c61ceccd71ba8e6cd862b6863b876aa Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <skh@uio.no> Date: Thu, 11 Nov 2021 15:32:51 +0100 Subject: [PATCH 1/9] Add the cerebrum-client to the requirments --- poetry.lock | 29 +++++++++++++++++++++++++---- pyproject.toml | 1 + 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index 84dfd0d4..2c491b9f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -119,6 +119,25 @@ jinxed = {version = ">=1.1.0", markers = "platform_system == \"Windows\""} six = ">=1.9.0" wcwidth = ">=0.1.4" +[[package]] +name = "cerebrum-client" +version = "1.9.2" +description = "Client for the Cerebrum REST API" +category = "main" +optional = false +python-versions = "*" +develop = false + +[package.dependencies] +pydantic = "*" +requests = "*" + +[package.source] +type = "git" +url = "https://git.app.uib.no/it-bott-integrasjoner/cerebrum-client.git" +reference = "v1.9.2" +resolved_reference = "95a183956ddd164f2a9fa86ed219ef640e72b7be" + [[package]] name = "certifi" version = "2021.10.8" @@ -1099,14 +1118,14 @@ test = ["fixtures", "mock", "purl", "pytest", "sphinx", "testrepository (>=0.0.1 [[package]] name = "rope" -version = "0.21.0" +version = "0.21.1" description = "a python refactoring library..." category = "dev" optional = false python-versions = "*" [package.extras] -dev = ["pytest", "pytest-timeout"] +dev = ["build", "pytest", "pytest-timeout"] [[package]] name = "sentry-sdk" @@ -1284,7 +1303,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "45574277e3b663b9eb9b18a24ef0996878944461d137b084199690e9683e32f7" +content-hash = "9887d688cf3c515273b5ccbc7313ba2795d6fbf6e6828091537761c376dbee87" [metadata.files] ansicon = [ @@ -1327,6 +1346,7 @@ blessed = [ {file = "blessed-1.19.0-py2.py3-none-any.whl", hash = "sha256:1f2d462631b2b6d2d4c3c65b54ef79ad87a6ca2dd55255df2f8d739fcc8a1ddb"}, {file = "blessed-1.19.0.tar.gz", hash = "sha256:4db0f94e5761aea330b528e84a250027ffe996b5a94bf03e502600c9a5ad7a61"}, ] +cerebrum-client = [] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, @@ -2012,7 +2032,8 @@ requests-mock = [ {file = "requests_mock-1.9.3-py2.py3-none-any.whl", hash = "sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970"}, ] rope = [ - {file = "rope-0.21.0.tar.gz", hash = "sha256:366789e069a267296889b2ee7631f9278173b5e7d468f2ea08abe26069a52aef"}, + {file = "rope-0.21.1-py3-none-any.whl", hash = "sha256:7fa433eb946bf806a419a3da354cd9b56d565cadf7159952c8e71c72d4b1a8ec"}, + {file = "rope-0.21.1.tar.gz", hash = "sha256:4fe61ea25ca64f1819be57fbb44ee07ca98b3dce08a0ceaaf3c6d6166b603f7f"}, ] sentry-sdk = [ {file = "sentry-sdk-1.4.3.tar.gz", hash = "sha256:b9844751e40710e84a457c5bc29b21c383ccb2b63d76eeaad72f7f1c808c8828"}, diff --git a/pyproject.toml b/pyproject.toml index 8759d509..dd848269 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ django-q = "^1.3.9" django-structlog = "^2.1.3" structlog = "^21.2.0" phonenumbers = "^8.12.35" +cerebrum-client = {git = "https://git.app.uib.no/it-bott-integrasjoner/cerebrum-client.git", rev = "v1.9.2"} [tool.poetry.dev-dependencies] Faker = "*" -- GitLab From 43cb3d8a7e981513346ca24bb6878a2930e82cc6 Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <skh@uio.no> Date: Thu, 11 Nov 2021 15:33:32 +0100 Subject: [PATCH 2/9] Do not typecheck the cerebrum-client --- mypy.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy.ini b/mypy.ini index 67b5755d..971a70a5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -14,6 +14,9 @@ django_settings_module = gregsite.settings.dev # Ignore problems in auto generated modules. ignore_errors = True +[mypy-cerebrum_client.*] +ignore_missing_imports = True + [mypy-dirtyfields] ignore_missing_imports = True -- GitLab From 299a6954194bda3ab46256ba1f5c93d1fa229e97 Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <skh@uio.no> Date: Thu, 11 Nov 2021 15:34:38 +0100 Subject: [PATCH 3/9] Add a automatic flag and source to the SponsorOrganizationalUnit model --- .../0014_add_sponsor_ou_source_data.py | 23 +++++++++++++++++++ greg/models.py | 2 ++ 2 files changed, 25 insertions(+) create mode 100644 greg/migrations/0014_add_sponsor_ou_source_data.py diff --git a/greg/migrations/0014_add_sponsor_ou_source_data.py b/greg/migrations/0014_add_sponsor_ou_source_data.py new file mode 100644 index 00000000..734591e8 --- /dev/null +++ b/greg/migrations/0014_add_sponsor_ou_source_data.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.9 on 2021-11-11 14:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('greg', '0013_delete_scheduletask'), + ] + + operations = [ + migrations.AddField( + model_name='sponsororganizationalunit', + name='automatic', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='sponsororganizationalunit', + name='source', + field=models.CharField(blank=True, max_length=256), + ), + ] diff --git a/greg/models.py b/greg/models.py index 168c69d3..90d54ca0 100644 --- a/greg/models.py +++ b/greg/models.py @@ -458,6 +458,8 @@ class SponsorOrganizationalUnit(BaseModel): "OrganizationalUnit", on_delete=models.PROTECT, related_name="link_unit" ) hierarchical_access = models.BooleanField() + automatic = models.BooleanField(default=False) + source = models.CharField(max_length=256, blank=True) class Meta: constraints = [ -- GitLab From 2a4db6bc236b3b7da819844702d2d3b2ed894684 Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <skh@uio.no> Date: Thu, 11 Nov 2021 15:43:25 +0100 Subject: [PATCH 4/9] Add example cerebrum_client config --- gregsite/settings/dev.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gregsite/settings/dev.py b/gregsite/settings/dev.py index 5120e02b..cae8f715 100644 --- a/gregsite/settings/dev.py +++ b/gregsite/settings/dev.py @@ -17,6 +17,11 @@ ORGREG_CLIENT = { "headers": {"X-Gravitee-Api-Key": "bar"}, } +CEREBRUM_CLIENT = { + "url": "https://example.com/fake/", + "headers": {"X-Gravitee-Api-Key": "bar"}, +} + Q_CLUSTER = { "name": "greg", "workers": 4, -- GitLab From 6995c681e2988d51330d7398fa4c267cf5372029 Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <skh@uio.no> Date: Mon, 15 Nov 2021 07:59:50 +0100 Subject: [PATCH 5/9] New command for importing sponsors from Cerebrum --- .../commands/import_sponsors_from_cerebrum.py | 212 ++++++++++++++++++ gregsite/settings/dev.py | 1 + 2 files changed, 213 insertions(+) create mode 100644 greg/management/commands/import_sponsors_from_cerebrum.py diff --git a/greg/management/commands/import_sponsors_from_cerebrum.py b/greg/management/commands/import_sponsors_from_cerebrum.py new file mode 100644 index 00000000..338592b6 --- /dev/null +++ b/greg/management/commands/import_sponsors_from_cerebrum.py @@ -0,0 +1,212 @@ +""" +Fetch all OUs from OrgReg and add the complete tree to Greg. + +Ignores OrganizationalUnits without identifiers with source and name matching global +variables ORGREG_SOURCE and ORGREG_NAME + +Assumes that the header used for authentication is of the type +'X-Gravitee-Api-Key': 'token'. + +If the path to the endpoint of the OUs is oregreg/v3/ou/ you want to give +orgreg/v3/ as the url argument (note the trailing slash). +""" +from typing import Optional, Tuple +import cerebrum_client + +import structlog + +from cerebrum_client import CerebrumClient +from django.conf import settings +from django.core.management.base import BaseCommand + +from greg.models import OrganizationalUnit, Sponsor, SponsorOrganizationalUnit + +logger = structlog.getLogger(__name__) + + +class Command(BaseCommand): + help = __doc__ + + CEREBRUM_SOURCE = "cerebrum" + CEREBRUM_VALID_SOURCE_SYSTEM = ["DFO_SAP"] + CEREBRUM_FEIDE_INST = "uio.no" + CEREBRUM_NAME_SOURCE_PRIORITY = ["Cached", "Override", "DFO_SAP", "FS", "Manual"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.client = CerebrumClient(**settings.CEREBRUM_CLIENT) + + def _has_active_dfo_aff(self, person_id: str): + """Check that a person has a valid employee affiliation from DFØ.""" + + dfo_employee_affs = [ + x + for x in self.client.get_person_affiliations(person_id) + if x.source_system == "DFO_SAP" and x.affiliation == "ANSATT" + ] + + return len(dfo_employee_affs) > 0 + + def _upsert_sponsor_unit_link(self, sponsor: Sponsor, unit: OrganizationalUnit): + """Ensure a link between sponsor and unit.""" + try: + sunit = SponsorOrganizationalUnit.objects.get( + sponsor=sponsor, + organizational_unit=unit, + source=self.CEREBRUM_SOURCE, + automatic=True, + ) + logger.info("sponsor_ou_link_found", sponsor=sponsor.id, sunit=sunit.id) + except SponsorOrganizationalUnit.DoesNotExist: + sunit = SponsorOrganizationalUnit.objects.create( + sponsor=sponsor, + organizational_unit=unit, + hierarchical_access=settings.CEREBRUM_HIERARCHICAL_ACCESS, + automatic=True, + source=self.CEREBRUM_SOURCE, + ) + logger.info("sponsor_ou_link_create", sponsor=sponsor.id, sunit=sunit.id) + return SponsorOrganizationalUnit.objects.get(id=sunit.id) + + def _remove_sponsor_unit_link(self, sunit: SponsorOrganizationalUnit): + logger.info("sponsor_ou_deleted", sunit=sunit.id) + sunit.delete() + + def _get_person_name( + self, person: cerebrum_client.models.Person + ) -> Tuple[Optional[str], Optional[str]]: + """Get a persons chosen name.""" + first_names = {x.source_system: x for x in person.names if x.variant == "FIRST"} + last_names = {x.source_system: x for x in person.names if x.variant == "LAST"} + + for source_system in self.CEREBRUM_NAME_SOURCE_PRIORITY: + if source_system in first_names and source_system in last_names: + return first_names[source_system].name, last_names[source_system].name + + return None, None + + def _get_feide_id(self, person_id: str) -> Optional[str]: + """Infer the feide id from the primary user.""" + primary_uname = self.client.get_person_primary_account_name(person_id) + + if primary_uname: + return f"{primary_uname}@{self.CEREBRUM_FEIDE_INST}" + return None + + def _upsert_sponsor_from_cerebrum( + self, person_id: str, unit: OrganizationalUnit + ) -> Optional[Sponsor]: + """Insert or update a sponsor from Cerebum data.""" + # logger.bind(cerebrum_person_id=person_id) + + person = self.client.get_person(person_id) + if not person: + logger.warning("cerebrum_person_missing", cerebrum_person_id=person_id) + return None + + if not self._has_active_dfo_aff(person_id): + logger.warning("cerebrum_not_an_employee", cerebrum_person_id=person_id) + return None + + feide_id = self._get_feide_id(person_id) + if not feide_id: + logger.warning("cerebrum_no_primary_account", cerebrum_person_id=person_id) + return None + + # log = log.bind(feide_id=feide_id) + + first_name, last_name = self._get_person_name(person) + if not first_name or not last_name: + logger.warning("cerebrum_no_valid_name", cerebrum_person_id=person_id) + return None + + try: + sponsor = Sponsor.objects.get(feide_id=feide_id) + sponsor.first_name = first_name + sponsor.last_name = last_name + sponsor.save() + logger.info( + "sponsor_updated", sponsor=sponsor.id, cerebrum_person_id=person_id + ) + + except Sponsor.DoesNotExist: + sponsor = Sponsor.objects.create( + first_name=first_name, + last_name=last_name, + feide_id=feide_id, + ) + logger.info( + "sponsor_created", sponsor=sponsor.id, cerebrum_person_id=person_id + ) + return Sponsor.objects.get(id=sponsor.id) + + def handle(self, *args, **options): + """Import of Sponsors from Cerebrum.""" + active_units = OrganizationalUnit.objects.filter( + active=True, + deleted=False, + ) + + logger.info("import_start", nr_of_units=len(active_units)) + for unit in active_units: + logger.bind(unit=unit.id) + sko = unit.identifiers.filter(name="legacy_stedkode").first() + if not sko: + logger.warning("orgreg_unit_missing_legacy_stedkode") + continue + logger.bind(legacy_stedkode=sko.value) + + current_sponsors = unit.link_unit.filter( + automatic=True, source=self.CEREBRUM_SOURCE + ).all() + + group_name = f"adm-leder-{sko.value}" + group = self.client.get_group(group_name) + if not group: + # No group in cererbum, remove sponsors. + logger.info( + "cerebrum_group_not_found", + unit_id=unit.id, + cerebrum_group=group_name, + ) + for sponsor in current_sponsors: + self._remove_sponsor_unit_link(non_cerebrum_sponsor) + continue + + if group.expire_date: + # Group is expired, remove sponsors + logger.info( + "cerebrum_group_expired", + unit_id=unit.id, + cerebrum_group=group_name, + ) + for sponsor in current_sponsors: + self._remove_sponsor_unit_link(non_cerebrum_sponsor) + continue + + group_members = list(self.client.list_group_members(group.id)) + if not group_members: + # No members in group, remove sponsors + logger.info( + "cerebrum_group_empty", unit_id=unit.id, cerebrum_group=group_name + ) + for sponsor in current_sponsors: + self._remove_sponsor_unit_link(non_cerebrum_sponsor) + continue + + cerebrum_sponsors = set() + for member in group_members: + if member.type == "person": + sponsor = self._upsert_sponsor_from_cerebrum(member.id, unit) + if sponsor: + sponsor_link = self._upsert_sponsor_unit_link( + sponsor=sponsor, unit=unit + ) + cerebrum_sponsors.add(sponsor_link) + + non_cerebrum_sponsors = set(current_sponsors) - cerebrum_sponsors + + for non_cerebrum_sponsor in non_cerebrum_sponsors: + self._remove_sponsor_unit_link(non_cerebrum_sponsor) + + logger.info("import_end") diff --git a/gregsite/settings/dev.py b/gregsite/settings/dev.py index cae8f715..e989299f 100644 --- a/gregsite/settings/dev.py +++ b/gregsite/settings/dev.py @@ -21,6 +21,7 @@ CEREBRUM_CLIENT = { "url": "https://example.com/fake/", "headers": {"X-Gravitee-Api-Key": "bar"}, } +CEREBRUM_HIERARCHICAL_ACCESS = True Q_CLUSTER = { "name": "greg", -- GitLab From 3c3aa400ba7c1af11b72811b4b3cd7a534c59337 Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <skh@uio.no> Date: Mon, 15 Nov 2021 08:26:16 +0100 Subject: [PATCH 6/9] Make the linter happy --- greg/management/commands/import_sponsors_from_cerebrum.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/greg/management/commands/import_sponsors_from_cerebrum.py b/greg/management/commands/import_sponsors_from_cerebrum.py index 338592b6..f14455f5 100644 --- a/greg/management/commands/import_sponsors_from_cerebrum.py +++ b/greg/management/commands/import_sponsors_from_cerebrum.py @@ -204,9 +204,7 @@ class Command(BaseCommand): ) cerebrum_sponsors.add(sponsor_link) - non_cerebrum_sponsors = set(current_sponsors) - cerebrum_sponsors - - for non_cerebrum_sponsor in non_cerebrum_sponsors: + for non_cerebrum_sponsor in set(current_sponsors) - cerebrum_sponsors: self._remove_sponsor_unit_link(non_cerebrum_sponsor) logger.info("import_end") -- GitLab From 8dd08ca23064e2efe5a9471cd089cb1b09fb8642 Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <skh@uio.no> Date: Mon, 15 Nov 2021 08:56:24 +0100 Subject: [PATCH 7/9] Bugfix and update doc --- .../commands/import_sponsors_from_cerebrum.py | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/greg/management/commands/import_sponsors_from_cerebrum.py b/greg/management/commands/import_sponsors_from_cerebrum.py index f14455f5..1c6e91da 100644 --- a/greg/management/commands/import_sponsors_from_cerebrum.py +++ b/greg/management/commands/import_sponsors_from_cerebrum.py @@ -1,15 +1,13 @@ """ -Fetch all OUs from OrgReg and add the complete tree to Greg. +Fetch sponsors for all units in GREG. -Ignores OrganizationalUnits without identifiers with source and name matching global -variables ORGREG_SOURCE and ORGREG_NAME +Uses the members of the adm-leder-<legacy_stedkode> groups to +populate Sponsors and SponsorOrganizationalUnits. -Assumes that the header used for authentication is of the type -'X-Gravitee-Api-Key': 'token'. - -If the path to the endpoint of the OUs is oregreg/v3/ou/ you want to give -orgreg/v3/ as the url argument (note the trailing slash). +This script does only remove the SponsorOrganizationalUnit. +The Sponsor objects are kept, even with no units """ + from typing import Optional, Tuple import cerebrum_client @@ -28,7 +26,6 @@ class Command(BaseCommand): help = __doc__ CEREBRUM_SOURCE = "cerebrum" - CEREBRUM_VALID_SOURCE_SYSTEM = ["DFO_SAP"] CEREBRUM_FEIDE_INST = "uio.no" CEREBRUM_NAME_SOURCE_PRIORITY = ["Cached", "Override", "DFO_SAP", "FS", "Manual"] @@ -170,7 +167,7 @@ class Command(BaseCommand): cerebrum_group=group_name, ) for sponsor in current_sponsors: - self._remove_sponsor_unit_link(non_cerebrum_sponsor) + self._remove_sponsor_unit_link(sponsor) continue if group.expire_date: @@ -181,17 +178,17 @@ class Command(BaseCommand): cerebrum_group=group_name, ) for sponsor in current_sponsors: - self._remove_sponsor_unit_link(non_cerebrum_sponsor) + self._remove_sponsor_unit_link(sponsor) continue - group_members = list(self.client.list_group_members(group.id)) + group_members = list(self.client.list_group_members(group_name)) if not group_members: # No members in group, remove sponsors logger.info( "cerebrum_group_empty", unit_id=unit.id, cerebrum_group=group_name ) for sponsor in current_sponsors: - self._remove_sponsor_unit_link(non_cerebrum_sponsor) + self._remove_sponsor_unit_link(sponsor) continue cerebrum_sponsors = set() @@ -204,7 +201,7 @@ class Command(BaseCommand): ) cerebrum_sponsors.add(sponsor_link) - for non_cerebrum_sponsor in set(current_sponsors) - cerebrum_sponsors: - self._remove_sponsor_unit_link(non_cerebrum_sponsor) + for sponsor in set(current_sponsors) - cerebrum_sponsors: + self._remove_sponsor_unit_link(sponsor) logger.info("import_end") -- GitLab From 28147c7b2cc77d30c8d880a943033cc3e71958ed Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <skh@uio.no> Date: Mon, 15 Nov 2021 09:49:50 +0100 Subject: [PATCH 8/9] Remove unused code --- greg/management/commands/import_sponsors_from_cerebrum.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/greg/management/commands/import_sponsors_from_cerebrum.py b/greg/management/commands/import_sponsors_from_cerebrum.py index 1c6e91da..be97fd72 100644 --- a/greg/management/commands/import_sponsors_from_cerebrum.py +++ b/greg/management/commands/import_sponsors_from_cerebrum.py @@ -94,8 +94,6 @@ class Command(BaseCommand): self, person_id: str, unit: OrganizationalUnit ) -> Optional[Sponsor]: """Insert or update a sponsor from Cerebum data.""" - # logger.bind(cerebrum_person_id=person_id) - person = self.client.get_person(person_id) if not person: logger.warning("cerebrum_person_missing", cerebrum_person_id=person_id) @@ -110,8 +108,6 @@ class Command(BaseCommand): logger.warning("cerebrum_no_primary_account", cerebrum_person_id=person_id) return None - # log = log.bind(feide_id=feide_id) - first_name, last_name = self._get_person_name(person) if not first_name or not last_name: logger.warning("cerebrum_no_valid_name", cerebrum_person_id=person_id) -- GitLab From 37fcabe51c168b774504a8f210b91e4f6a9ebff4 Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <skh@uio.no> Date: Mon, 15 Nov 2021 09:58:53 +0100 Subject: [PATCH 9/9] Use get_or_create --- .../commands/import_sponsors_from_cerebrum.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/greg/management/commands/import_sponsors_from_cerebrum.py b/greg/management/commands/import_sponsors_from_cerebrum.py index be97fd72..7139e115 100644 --- a/greg/management/commands/import_sponsors_from_cerebrum.py +++ b/greg/management/commands/import_sponsors_from_cerebrum.py @@ -46,23 +46,19 @@ class Command(BaseCommand): def _upsert_sponsor_unit_link(self, sponsor: Sponsor, unit: OrganizationalUnit): """Ensure a link between sponsor and unit.""" - try: - sunit = SponsorOrganizationalUnit.objects.get( - sponsor=sponsor, - organizational_unit=unit, - source=self.CEREBRUM_SOURCE, - automatic=True, - ) - logger.info("sponsor_ou_link_found", sponsor=sponsor.id, sunit=sunit.id) - except SponsorOrganizationalUnit.DoesNotExist: - sunit = SponsorOrganizationalUnit.objects.create( - sponsor=sponsor, - organizational_unit=unit, - hierarchical_access=settings.CEREBRUM_HIERARCHICAL_ACCESS, - automatic=True, - source=self.CEREBRUM_SOURCE, - ) + sunit, created = SponsorOrganizationalUnit.objects.get_or_create( + sponsor=sponsor, + organizational_unit=unit, + source=self.CEREBRUM_SOURCE, + automatic=True, + ) + if created: logger.info("sponsor_ou_link_create", sponsor=sponsor.id, sunit=sunit.id) + else: + logger.info("sponsor_ou_link_found", sponsor=sponsor.id, sunit=sunit.id) + sunit.hierarchical_access = settings.CEREBRUM_HIERARCHICAL_ACCESS + sunit.save() + return SponsorOrganizationalUnit.objects.get(id=sunit.id) def _remove_sponsor_unit_link(self, sunit: SponsorOrganizationalUnit): -- GitLab