From 27911772d474bd73f502c0a8d444aa0edd8f6fe4 Mon Sep 17 00:00:00 2001 From: Alexander Saprykin Date: Thu, 15 Feb 2024 17:39:30 +0000 Subject: [PATCH] Add organization and team models * Add new `Oranization` and `Team` models, that implement respective abstract models from DAB. * Add related database migrations and data migration, that creates a new default model. * Implement custom manager for the `Organization` model that retrieves the default organization. * Team model has a one-to-one relationship with the standard Group model. * When new Team record is created, a related Group model is automatically created as well. No-Issue --- galaxy_ng/app/api/resource_api.py | 23 ++- galaxy_ng/app/migrations/0049_organization.py | 176 ++++++++++++++++++ .../app/migrations/0050_organization_data.py | 55 ++++++ galaxy_ng/app/models/__init__.py | 63 +++---- galaxy_ng/app/models/organization.py | 65 +++++++ galaxy_ng/app/settings.py | 4 + 6 files changed, 341 insertions(+), 45 deletions(-) create mode 100644 galaxy_ng/app/migrations/0049_organization.py create mode 100644 galaxy_ng/app/migrations/0050_organization_data.py create mode 100644 galaxy_ng/app/models/organization.py diff --git a/galaxy_ng/app/api/resource_api.py b/galaxy_ng/app/api/resource_api.py index 8bbe04270d..a753dd2a36 100644 --- a/galaxy_ng/app/api/resource_api.py +++ b/galaxy_ng/app/api/resource_api.py @@ -3,7 +3,7 @@ ServiceAPIConfig, SharedResource, ) -from ansible_base.resource_registry.shared_types import UserType, TeamType +from ansible_base.resource_registry.shared_types import UserType, TeamType, OrganizationType from galaxy_ng.app import models @@ -14,18 +14,17 @@ class APIConfig(ServiceAPIConfig): RESOURCE_LIST = ( ResourceConfig( models.auth.User, - shared_resource=SharedResource( - serializer=UserType, - is_provider=False - ), - name_field="username" + shared_resource=SharedResource(serializer=UserType, is_provider=False), + name_field="username", ), ResourceConfig( - models.auth.Group, - shared_resource=SharedResource( - serializer=TeamType, - is_provider=True - ), - name_field="name" + models.Team, + shared_resource=SharedResource(serializer=TeamType, is_provider=True), + name_field="name", ), + ResourceConfig( + models.Organization, + shared_resource=SharedResource(serializer=OrganizationType, is_provider=False), + name_field="name", + ) ) diff --git a/galaxy_ng/app/migrations/0049_organization.py b/galaxy_ng/app/migrations/0049_organization.py new file mode 100644 index 0000000000..244caaff36 --- /dev/null +++ b/galaxy_ng/app/migrations/0049_organization.py @@ -0,0 +1,176 @@ +# Generated by Django 4.2.10 on 2024-02-15 17:33 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("galaxy", "0048_update_collection_remote_rhcertified_url"), + ] + + operations = [ + migrations.CreateModel( + name="Organization", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "created_on", + models.DateTimeField( + default=None, + editable=False, + help_text="The date/time this resource was created", + ), + ), + ( + "modified_on", + models.DateTimeField( + default=None, + editable=False, + help_text="The date/time this resource was created", + ), + ), + ( + "name", + models.CharField( + help_text="The name of this resource", max_length=512, unique=True + ), + ), + ( + "description", + models.TextField( + blank=True, default="", help_text="The organization description." + ), + ), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who created this resource", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="%(app_label)s_%(class)s_created+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who last modified this resource", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="%(app_label)s_%(class)s_modified+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "users", + models.ManyToManyField( + help_text="The list of users in this organization.", + related_name="organizations", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Team", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "created_on", + models.DateTimeField( + default=None, + editable=False, + help_text="The date/time this resource was created", + ), + ), + ( + "modified_on", + models.DateTimeField( + default=None, + editable=False, + help_text="The date/time this resource was created", + ), + ), + ("name", models.CharField(help_text="The name of this resource", max_length=512)), + ( + "description", + models.TextField(blank=True, default="", help_text="The team description."), + ), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who created this resource", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="%(app_label)s_%(class)s_created+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who last modified this resource", + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="%(app_label)s_%(class)s_modified+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "organization", + models.ForeignKey( + help_text="The organization of this team.", + on_delete=django.db.models.deletion.CASCADE, + related_name="teams", + to="galaxy.organization", + ), + ), + ( + "users", + models.ManyToManyField( + help_text="The list of users in this team.", + related_name="teams", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "group", + models.OneToOneField( + help_text="Related group record.", + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="galaxy.group", + ), + + ) + ], + options={ + "ordering": ("organization__name", "name"), + "abstract": False, + "unique_together": {("organization", "name")}, + }, + ), + ] diff --git a/galaxy_ng/app/migrations/0050_organization_data.py b/galaxy_ng/app/migrations/0050_organization_data.py new file mode 100644 index 0000000000..4c80cd0450 --- /dev/null +++ b/galaxy_ng/app/migrations/0050_organization_data.py @@ -0,0 +1,55 @@ +from django.conf import settings +from django.db import migrations +from django.utils import timezone + + +def create_default_organization(apps, schema_editor): + db_alias = schema_editor.connection.alias + Organization = apps.get_model("galaxy", "Organization") + + now = timezone.now() + org = Organization.objects.using(db_alias).create( + name=settings.DEFAULT_ORGANIZATION_NAME, + description="A default organization.", + created_on=now, + modified_on=now, + ) + + schema_editor.execute(""" + INSERT INTO galaxy_team (name, description, created_on, modified_on, group_id, organization_id) + SELECT grp.name, '', now(), now(), grp.id, %s + FROM auth_group AS grp + """, (org.id,)) + + schema_editor.execute(""" + UPDATE auth_group SET name = %s || '::' || name + """, (settings.DEFAULT_ORGANIZATION_NAME,)) + + +def delete_default_organization(apps, schema_editor): + db_alias = schema_editor.connection.alias + Team = apps.get_model("galaxy", "Team") + Organization = apps.get_model("galaxy", "Organization") + + schema_editor.execute(""" + UPDATE auth_group SET name = regexp_replace(name, '^.+?::', '') + """) + + Team.objects.using(db_alias).delete() + + Organization.objects.using(db_alias).filter( + name=settings.DEFAULT_ORGANIZATION_NAME + ).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("galaxy", "0049_organization"), + ] + + operations = [ + migrations.RunPython( + code=create_default_organization, + reverse_code=delete_default_organization, + ) + ] diff --git a/galaxy_ng/app/models/__init__.py b/galaxy_ng/app/models/__init__.py index af71ee1fe3..57fc33abc8 100644 --- a/galaxy_ng/app/models/__init__.py +++ b/galaxy_ng/app/models/__init__.py @@ -1,43 +1,40 @@ -from .auth import ( - Group, - User, -) -from .collectionimport import ( - CollectionImport, -) +from .aiindex import AIIndexDenyList +from .auth import Group, User +from .collectionimport import CollectionImport from .config import Setting -from .namespace import ( - Namespace, - NamespaceLink, -) - -from .synclist import ( - SyncList, -) - from .container import ( ContainerDistribution, ContainerDistroReadme, ContainerNamespace, ContainerRegistryRemote, - ContainerRegistryRepos - + ContainerRegistryRepos, ) - -from .aiindex import AIIndexDenyList +from .namespace import Namespace, NamespaceLink +from .organization import Organization, Team +from .synclist import SyncList __all__ = ( - 'Group', - 'User', - 'CollectionImport', - 'Namespace', - 'NamespaceLink', - 'Setting', - 'SyncList', - 'ContainerDistribution', - 'ContainerDistroReadme', - 'ContainerNamespace', - 'ContainerRegistryRemote', - 'ContainerRegistryRepos', - 'AIIndexDenyList', + # aiindex + "AIIndexDenyList", + # auth + "Group", + "User", + # collectionimport + "CollectionImport", + # config + "Setting", + # container + "ContainerDistribution", + "ContainerDistroReadme", + "ContainerNamespace", + "ContainerRegistryRemote", + "ContainerRegistryRepos", + # namespace + "Namespace", + "NamespaceLink", + # organization + "Organization", + "Team", + # synclist + "SyncList", ) diff --git a/galaxy_ng/app/models/organization.py b/galaxy_ng/app/models/organization.py new file mode 100644 index 0000000000..5ff900c5e1 --- /dev/null +++ b/galaxy_ng/app/models/organization.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from ansible_base.lib.abstract_models import AbstractOrganization, AbstractTeam +from django.conf import settings +from django.db import models +from django_lifecycle import LifecycleModelMixin, hook, BEFORE_CREATE, AFTER_UPDATE + +from galaxy_ng.app.models.auth import Group + + +class OrganizationManager(models.Manager): + + def get_default(self) -> Organization: + """Return default organization.""" + return self.get(name=settings.DEFAULT_ORGANIZATION_NAME) + + +class Organization(LifecycleModelMixin, AbstractOrganization): + """An organization model.""" + + users = models.ManyToManyField( + settings.AUTH_USER_MODEL, + related_name="organizations", + help_text="The list of users in this organization." + ) + + objects = OrganizationManager() + + @hook(AFTER_UPDATE) + def _after_update(self): + if self.has_changed("name"): + for team in self.teams.all(): + group = team.group + group.name = team.group_name() + group.save() + + +class Team(LifecycleModelMixin, AbstractTeam): + """A team model.""" + + users = models.ManyToManyField( + settings.AUTH_USER_MODEL, + related_name="teams", + help_text="The list of users in this team." + ) + group = models.OneToOneField( + Group, + on_delete=models.CASCADE, + related_name='+', + help_text="Related group record.", + ) + + def group_name(self): + return f"{self.organization.name}::{self.name}" + + @hook(BEFORE_CREATE) + def _before_create(self): + if not hasattr(self, "group"): + self.group = Group.objects.create(name=self.group_name()) + + @hook(AFTER_UPDATE) + def _after_update(self): + if self.has_changed("name"): + self.group.name = self.group_name() + self.group.save() diff --git a/galaxy_ng/app/settings.py b/galaxy_ng/app/settings.py index db4a30a95f..17b0bef1e5 100644 --- a/galaxy_ng/app/settings.py +++ b/galaxy_ng/app/settings.py @@ -299,3 +299,7 @@ # DJANGO ANSIBLE BASE RESOURCES REGISTRY SETTINGS ANSIBLE_BASE_RESOURCE_CONFIG_MODULE = "galaxy_ng.app.api.resource_api" +ANSIBLE_BASE_ORGANIZATION_MODEL = "galaxy.Organization" + +# WARNING: This setting is used in database migrations to create a default organization. +DEFAULT_ORGANIZATION_NAME = "Default"