From 28415433daff0603c508175091f9b40c2611ff34 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Mon, 1 Jul 2024 15:36:15 -0700 Subject: [PATCH 1/9] Add an adapter method that runs after a workspace is created Add a new adapter method for Workspaces that runs after a workspace is created on AnVIL. --- .../adapters/workspace.py | 4 ++ .../tests/test_app/adapters.py | 19 +++++++ .../tests/test_app/forms.py | 8 +++ ...oricaltestworkspacemethodsdata_and_more.py | 49 +++++++++++++++++++ .../tests/test_app/models.py | 6 +++ anvil_consortium_manager/tests/test_views.py | 41 +++++++++++++++- anvil_consortium_manager/views.py | 2 + 7 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 anvil_consortium_manager/tests/test_app/migrations/0005_historicaltestworkspacemethodsdata_and_more.py diff --git a/anvil_consortium_manager/adapters/workspace.py b/anvil_consortium_manager/adapters/workspace.py index 2efd3942..44272c3c 100644 --- a/anvil_consortium_manager/adapters/workspace.py +++ b/anvil_consortium_manager/adapters/workspace.py @@ -149,6 +149,10 @@ def get_extra_detail_context_data(self, workspace, request): return {} + def after_workspace_create(self, workspace): + """Custom actions to take after a workspace is created.""" + pass + class AdapterAlreadyRegisteredError(Exception): """Exception raised when an adapter or its type is already registered.""" diff --git a/anvil_consortium_manager/tests/test_app/adapters.py b/anvil_consortium_manager/tests/test_app/adapters.py index 81454a60..746f50da 100644 --- a/anvil_consortium_manager/tests/test_app/adapters.py +++ b/anvil_consortium_manager/tests/test_app/adapters.py @@ -61,3 +61,22 @@ class TestForeignKeyWorkspaceAdapter(BaseWorkspaceAdapter): workspace_data_model = models.TestForeignKeyWorkspaceData workspace_data_form_class = forms.TestForeignKeyWorkspaceDataForm workspace_detail_template_name = "workspace_detail.html" + + +class TestWorkspaceMethodsAdapter(BaseWorkspaceAdapter): + """Test adapter for workspaces with custom methods defined.""" + + name = "Test methods workspace" + type = "test_methods" + description = "Workspace type for testing additional methods" + list_table_class_staff_view = WorkspaceStaffTable + list_table_class_view = WorkspaceUserTable + workspace_form_class = WorkspaceForm + workspace_data_model = models.TestWorkspaceMethodsData + workspace_data_form_class = forms.TestWorkspaceMethodsForm + workspace_detail_template_name = "workspace_detail.html" + + def after_workspace_create(self, workspace): + # Set the extra field to "FOO" + workspace.testworkspacemethodsdata.test_field = "FOO" + workspace.testworkspacemethodsdata.save() diff --git a/anvil_consortium_manager/tests/test_app/forms.py b/anvil_consortium_manager/tests/test_app/forms.py index fefff797..55a1c4ec 100644 --- a/anvil_consortium_manager/tests/test_app/forms.py +++ b/anvil_consortium_manager/tests/test_app/forms.py @@ -36,3 +36,11 @@ class TestForeignKeyWorkspaceDataForm(forms.ModelForm): class Meta: model = models.TestForeignKeyWorkspaceData fields = ("other_workspace", "workspace") + + +class TestWorkspaceMethodsForm(forms.ModelForm): + """Form for a TestWorkspaceMethods object.""" + + class Meta: + model = models.TestWorkspaceMethodsData + fields = ("test_field", "workspace") diff --git a/anvil_consortium_manager/tests/test_app/migrations/0005_historicaltestworkspacemethodsdata_and_more.py b/anvil_consortium_manager/tests/test_app/migrations/0005_historicaltestworkspacemethodsdata_and_more.py new file mode 100644 index 00000000..d57b9ff0 --- /dev/null +++ b/anvil_consortium_manager/tests/test_app/migrations/0005_historicaltestworkspacemethodsdata_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 5.0 on 2024-07-01 22:21 + +import django.db.models.deletion +import simple_history.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('anvil_consortium_manager', '0019_accountuserarchive'), + ('test_app', '0004_testforeignkeyworkspacedata_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalTestWorkspaceMethodsData', + fields=[ + ('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('test_field', models.CharField(max_length=255)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('workspace', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='anvil_consortium_manager.workspace')), + ], + options={ + 'verbose_name': 'historical test workspace methods data', + 'verbose_name_plural': 'historical test workspace methods datas', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='TestWorkspaceMethodsData', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('test_field', models.CharField(max_length=255)), + ('workspace', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='anvil_consortium_manager.workspace')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/anvil_consortium_manager/tests/test_app/models.py b/anvil_consortium_manager/tests/test_app/models.py index 3975262c..a75182c0 100644 --- a/anvil_consortium_manager/tests/test_app/models.py +++ b/anvil_consortium_manager/tests/test_app/models.py @@ -32,3 +32,9 @@ class TestForeignKeyWorkspaceData(BaseWorkspaceData): """Custom model with a second fk to Workspace.""" other_workspace = models.ForeignKey(Workspace, related_name="test_foreign_key_workspaces", on_delete=models.PROTECT) + + +class TestWorkspaceMethodsData(BaseWorkspaceData): + """Custom model with additional fields.""" + + test_field = models.CharField(max_length=255) diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py index 1d33d104..f44d4c2a 100644 --- a/anvil_consortium_manager/tests/test_views.py +++ b/anvil_consortium_manager/tests/test_views.py @@ -29,7 +29,7 @@ from .test_app import forms as app_forms from .test_app import models as app_models from .test_app import tables as app_tables -from .test_app.adapters import TestForeignKeyWorkspaceAdapter, TestWorkspaceAdapter +from .test_app.adapters import TestForeignKeyWorkspaceAdapter, TestWorkspaceAdapter, TestWorkspaceMethodsAdapter from .test_app.factories import TestWorkspaceDataFactory from .test_app.filters import TestAccountListFilter from .utils import AnVILAPIMockTestMixin, TestCase # Redefined to work with Django < 4.2 and Django=4.2. @@ -8273,6 +8273,45 @@ def test_post_workspace_data_with_second_foreign_key_to_workspace(self): self.assertEqual(new_workspace_data.workspace, new_workspace) self.assertEqual(new_workspace_data.other_workspace, other_workspace) + def test_post_custom_adapter_after_workspace_create(self): + """The after_workspace_create method is run after a workspace is.""" + # Overriding settings doesn't work, because appconfig.ready has already run and + # registered the default adapter. Instead, unregister the default and register the + # new adapter here. + workspace_adapter_registry.register(TestWorkspaceMethodsAdapter) + self.workspace_type = TestWorkspaceMethodsAdapter().get_type() + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + json_data = { + "namespace": "test-billing-project", + "name": "test-workspace", + "attributes": {}, + } + self.anvil_response_mock.add( + responses.POST, + self.api_url, + status=self.api_success_code, + match=[responses.matchers.json_params_matcher(json_data)], + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.workspace_type), + { + "billing_project": billing_project.pk, + "name": "test-workspace", + # Default workspace data for formset. + "workspacedata-TOTAL_FORMS": 1, + "workspacedata-INITIAL_FORMS": 0, + "workspacedata-MIN_NUM_FORMS": 1, + "workspacedata-MAX_NUM_FORMS": 1, + "workspacedata-0-test_field": "my field value", + }, + ) + self.assertEqual(response.status_code, 302) + # The workspace is created. + new_workspace = models.Workspace.objects.latest("pk") + # The test_field field was modified by the adapter. + self.assertEqual(new_workspace.testworkspacemethodsdata.test_field, "FOO") + class WorkspaceImportTest(AnVILAPIMockTestMixin, TestCase): """Tests for the WorkspaceImport view.""" diff --git a/anvil_consortium_manager/views.py b/anvil_consortium_manager/views.py index a1f6376b..fed3be9a 100644 --- a/anvil_consortium_manager/views.py +++ b/anvil_consortium_manager/views.py @@ -1065,6 +1065,8 @@ def form_valid(self, form): workspace_data_formset.forms[0].save() # Then create the workspace on AnVIL. self.workspace.anvil_create() + # Run the custom adapter method after a workspace is created. + self.adapter.after_workspace_create(self.workspace) except AnVILAPIError as e: # If the API call failed, rerender the page with the responses and show a message. messages.add_message(self.request, messages.ERROR, "AnVIL API Error: " + str(e)) From 84030b15e3f5431e3436238bb6d0faf44830c0d2 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 14:13:40 -0700 Subject: [PATCH 2/9] Add an adapter method that runs before a workspace is created --- .../adapters/workspace.py | 6 ++- anvil_consortium_manager/forms.py | 1 - .../tests/test_app/adapters.py | 21 ++++++-- anvil_consortium_manager/tests/test_views.py | 51 +++++++++++++++++-- anvil_consortium_manager/views.py | 6 +-- example_site/app/adapters.py | 10 ++++ example_site/app/forms.py | 12 ++++- 7 files changed, 93 insertions(+), 14 deletions(-) diff --git a/anvil_consortium_manager/adapters/workspace.py b/anvil_consortium_manager/adapters/workspace.py index 44272c3c..babce64e 100644 --- a/anvil_consortium_manager/adapters/workspace.py +++ b/anvil_consortium_manager/adapters/workspace.py @@ -149,8 +149,12 @@ def get_extra_detail_context_data(self, workspace, request): return {} + def before_workspace_create(self, workspace): + """Custom actions to take after a workspace is created on AnVIL.""" + pass + def after_workspace_create(self, workspace): - """Custom actions to take after a workspace is created.""" + """Custom actions to take after a workspace is created on AnVIL.""" pass diff --git a/anvil_consortium_manager/forms.py b/anvil_consortium_manager/forms.py index e17d8424..b3081a36 100644 --- a/anvil_consortium_manager/forms.py +++ b/anvil_consortium_manager/forms.py @@ -181,7 +181,6 @@ def clean(self): # Check for the same case insensitive name in the same billing project. is_mysql = settings.DATABASES["default"]["ENGINE"] == "django.db.backends.mysql" if not is_mysql: - print("here") billing_project = self.cleaned_data.get("billing_project", None) name = self.cleaned_data.get("name", None) if ( diff --git a/anvil_consortium_manager/tests/test_app/adapters.py b/anvil_consortium_manager/tests/test_app/adapters.py index 746f50da..45b084c7 100644 --- a/anvil_consortium_manager/tests/test_app/adapters.py +++ b/anvil_consortium_manager/tests/test_app/adapters.py @@ -64,11 +64,11 @@ class TestForeignKeyWorkspaceAdapter(BaseWorkspaceAdapter): class TestWorkspaceMethodsAdapter(BaseWorkspaceAdapter): - """Test adapter for workspaces with custom methods defined.""" + """Adapter superclass for testing adapter methods.""" - name = "Test methods workspace" - type = "test_methods" - description = "Workspace type for testing additional methods" + name = "workspace adapter methods testing" + type = "methods_tester" + description = "Workspace type for testing custom adapter methods method" list_table_class_staff_view = WorkspaceStaffTable list_table_class_view = WorkspaceUserTable workspace_form_class = WorkspaceForm @@ -76,6 +76,19 @@ class TestWorkspaceMethodsAdapter(BaseWorkspaceAdapter): workspace_data_form_class = forms.TestWorkspaceMethodsForm workspace_detail_template_name = "workspace_detail.html" + +class TestBeforeWorkspaceCreateAdapter(TestWorkspaceMethodsAdapter): + """Test adapter for workspaces with custom methods defined.""" + + def before_workspace_create(self, workspace): + # Append a -2 to the name of the workspace. + workspace.name = workspace.name + "-2" + workspace.save() + + +class TestAfterWorkspaceCreateAdapter(TestWorkspaceMethodsAdapter): + """Test adapter for workspaces with custom methods defined.""" + def after_workspace_create(self, workspace): # Set the extra field to "FOO" workspace.testworkspacemethodsdata.test_field = "FOO" diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py index f44d4c2a..3a39cb26 100644 --- a/anvil_consortium_manager/tests/test_views.py +++ b/anvil_consortium_manager/tests/test_views.py @@ -29,7 +29,12 @@ from .test_app import forms as app_forms from .test_app import models as app_models from .test_app import tables as app_tables -from .test_app.adapters import TestForeignKeyWorkspaceAdapter, TestWorkspaceAdapter, TestWorkspaceMethodsAdapter +from .test_app.adapters import ( + TestAfterWorkspaceCreateAdapter, + TestBeforeWorkspaceCreateAdapter, + TestForeignKeyWorkspaceAdapter, + TestWorkspaceAdapter, +) from .test_app.factories import TestWorkspaceDataFactory from .test_app.filters import TestAccountListFilter from .utils import AnVILAPIMockTestMixin, TestCase # Redefined to work with Django < 4.2 and Django=4.2. @@ -8273,13 +8278,51 @@ def test_post_workspace_data_with_second_foreign_key_to_workspace(self): self.assertEqual(new_workspace_data.workspace, new_workspace) self.assertEqual(new_workspace_data.other_workspace, other_workspace) + def test_post_custom_adapter_before_workspace_create(self): + """The before_workspace_create method is run before a workspace is created.""" + # Overriding settings doesn't work, because appconfig.ready has already run and + # registered the default adapter. Instead, unregister the default and register the + # new adapter here. + workspace_adapter_registry.register(TestBeforeWorkspaceCreateAdapter) + self.workspace_type = TestBeforeWorkspaceCreateAdapter().get_type() + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + json_data = { + "namespace": "test-billing-project", + "name": "test-workspace-2", + "attributes": {}, + } + self.anvil_response_mock.add( + responses.POST, + self.api_url, + status=self.api_success_code, + match=[responses.matchers.json_params_matcher(json_data)], + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.workspace_type), + { + "billing_project": billing_project.pk, + "name": "test-workspace", + # Default workspace data for formset. + "workspacedata-TOTAL_FORMS": 1, + "workspacedata-INITIAL_FORMS": 0, + "workspacedata-MIN_NUM_FORMS": 1, + "workspacedata-MAX_NUM_FORMS": 1, + "workspacedata-0-test_field": "my field value", + }, + ) + self.assertEqual(response.status_code, 302) + # The workspace is created. + new_workspace = models.Workspace.objects.latest("pk") + self.assertEqual(new_workspace.name, "test-workspace-2") + def test_post_custom_adapter_after_workspace_create(self): - """The after_workspace_create method is run after a workspace is.""" + """The after_workspace_create method is run after a workspace is created.""" # Overriding settings doesn't work, because appconfig.ready has already run and # registered the default adapter. Instead, unregister the default and register the # new adapter here. - workspace_adapter_registry.register(TestWorkspaceMethodsAdapter) - self.workspace_type = TestWorkspaceMethodsAdapter().get_type() + workspace_adapter_registry.register(TestAfterWorkspaceCreateAdapter) + self.workspace_type = TestAfterWorkspaceCreateAdapter().get_type() billing_project = factories.BillingProjectFactory.create(name="test-billing-project") json_data = { "namespace": "test-billing-project", diff --git a/anvil_consortium_manager/views.py b/anvil_consortium_manager/views.py index fed3be9a..5d9e6f31 100644 --- a/anvil_consortium_manager/views.py +++ b/anvil_consortium_manager/views.py @@ -1060,12 +1060,12 @@ def form_valid(self, form): transaction.set_rollback(True) return self.forms_invalid(form, workspace_data_formset) # Now save the auth domains and the workspace_data_form. - for auth_domain in form.cleaned_data["authorization_domains"]: + for auth_domain in form.cleaned_data.get("authorization_domains", []): models.WorkspaceAuthorizationDomain.objects.create(workspace=self.workspace, group=auth_domain) workspace_data_formset.forms[0].save() - # Then create the workspace on AnVIL. + # Then create the workspace on AnVIL, running custom adapter methods as appropriate. + self.adapter.before_workspace_create(self.workspace) self.workspace.anvil_create() - # Run the custom adapter method after a workspace is created. self.adapter.after_workspace_create(self.workspace) except AnVILAPIError as e: # If the API call failed, rerender the page with the responses and show a message. diff --git a/example_site/app/adapters.py b/example_site/app/adapters.py index bc3d3e45..dbfcb034 100644 --- a/example_site/app/adapters.py +++ b/example_site/app/adapters.py @@ -1,4 +1,5 @@ from anvil_consortium_manager.adapters.workspace import BaseWorkspaceAdapter +from anvil_consortium_manager.models import ManagedGroup from . import forms, models, tables @@ -15,3 +16,12 @@ class CustomWorkspaceAdapter(BaseWorkspaceAdapter): workspace_data_model = models.CustomWorkspaceData workspace_data_form_class = forms.CustomWorkspaceDataForm workspace_detail_template_name = "app/custom_workspace_detail.html" + + def before_workspace_create(self, workspace): + """Add authorization domain to workspace.""" + auth_domain_name = "AUTH_" + workspace.name + auth_domain = ManagedGroup.objects.create( + name=auth_domain_name, is_managed_by_app=True, email=auth_domain_name + "@firecloud.org" + ) + workspace.authorization_domains.add(auth_domain) + auth_domain.anvil_create() diff --git a/example_site/app/forms.py b/example_site/app/forms.py index fd366313..3275c435 100644 --- a/example_site/app/forms.py +++ b/example_site/app/forms.py @@ -9,11 +9,21 @@ class CustomWorkspaceForm(WorkspaceForm): - """Example custom form for creating a Workspace.""" + """Example custom form for creating a Workspace. + + This form has a custom clean method that does not allow a "y" in the workspace name. + It also disables the authorization_domains field.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["authorization_domains"].disabled = True class Meta(WorkspaceForm.Meta): help_texts = { "name": "Enter the name of the workspace to create. (Hint: Example workspace names cannot include a 'y'.)", + "authorization_domains": ( + "An authorization domain will be automatically created " "using the name of the workspace." + ), } def clean_name(self): From 814b9023ad324f7c3e5512bcb3187d97bac5a31e Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 14:19:45 -0700 Subject: [PATCH 3/9] Call the before_ and after_workspace_create methods when cloning When cloning a workspace in using the WorkspaceClone view, call the before_workspace_create and after_workspace_create methods where appropriate. --- anvil_consortium_manager/tests/test_views.py | 87 ++++++++++++++++++++ anvil_consortium_manager/views.py | 4 +- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py index 3a39cb26..f494f6ab 100644 --- a/anvil_consortium_manager/tests/test_views.py +++ b/anvil_consortium_manager/tests/test_views.py @@ -10921,6 +10921,93 @@ def test_post_workspace_data_with_second_foreign_key_to_workspace(self): self.assertEqual(new_workspace_data.workspace, new_workspace) self.assertEqual(new_workspace_data.other_workspace, other_workspace) + def test_post_custom_adapter_before_workspace_create(self): + """The before_workspace_create method is run before a workspace is created.""" + # Overriding settings doesn't work, because appconfig.ready has already run and + # registered the default adapter. Instead, unregister the default and register the + # new adapter here. + workspace_adapter_registry.register(TestBeforeWorkspaceCreateAdapter) + self.workspace_type = TestBeforeWorkspaceCreateAdapter().get_type() + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + json_data = { + "namespace": "test-billing-project", + "name": "test-workspace-2", + "attributes": {}, + "copyFilesWithPrefix": "notebooks", + } + self.anvil_response_mock.add( + responses.POST, + self.api_url, + status=self.api_success_code, + match=[responses.matchers.json_params_matcher(json_data)], + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url( + self.workspace_to_clone.billing_project.name, + self.workspace_to_clone.name, + self.workspace_type, + ), + { + "billing_project": billing_project.pk, + "name": "test-workspace", + # Default workspace data for formset. + "workspacedata-TOTAL_FORMS": 1, + "workspacedata-INITIAL_FORMS": 0, + "workspacedata-MIN_NUM_FORMS": 1, + "workspacedata-MAX_NUM_FORMS": 1, + "workspacedata-0-test_field": "my field value", + }, + ) + self.assertEqual(response.status_code, 302) + # The workspace is created. + new_workspace = models.Workspace.objects.latest("pk") + self.assertEqual(new_workspace.name, "test-workspace-2") + + def test_post_custom_adapter_after_workspace_create(self): + """The after_workspace_create method is run after a workspace is created.""" + # Overriding settings doesn't work, because appconfig.ready has already run and + # registered the default adapter. Instead, unregister the default and register the + # new adapter here. + workspace_adapter_registry.register(TestAfterWorkspaceCreateAdapter) + self.workspace_type = TestAfterWorkspaceCreateAdapter().get_type() + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + json_data = { + "namespace": "test-billing-project", + "name": "test-workspace", + "attributes": {}, + "copyFilesWithPrefix": "notebooks", + } + self.anvil_response_mock.add( + responses.POST, + self.api_url, + status=self.api_success_code, + match=[responses.matchers.json_params_matcher(json_data)], + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url( + self.workspace_to_clone.billing_project.name, + self.workspace_to_clone.name, + self.workspace_type, + ), + { + "billing_project": billing_project.pk, + "name": "test-workspace", + # Default workspace data for formset. + "workspacedata-TOTAL_FORMS": 1, + "workspacedata-INITIAL_FORMS": 0, + "workspacedata-MIN_NUM_FORMS": 1, + "workspacedata-MAX_NUM_FORMS": 1, + "workspacedata-0-test_field": "my field value", + }, + ) + self.assertEqual(response.status_code, 302) + # The workspace is created. + new_workspace = models.Workspace.objects.latest("pk") + # The test_field field was modified by the adapter. + self.assertEqual(new_workspace.testworkspacemethodsdata.test_field, "FOO") + class WorkspaceUpdateTest(TestCase): def setUp(self): diff --git a/anvil_consortium_manager/views.py b/anvil_consortium_manager/views.py index 5d9e6f31..5973ae37 100644 --- a/anvil_consortium_manager/views.py +++ b/anvil_consortium_manager/views.py @@ -1327,16 +1327,18 @@ def form_valid(self, form): transaction.set_rollback(True) return self.forms_invalid(form, workspace_data_formset) # Now save the auth domains and the workspace_data_form. - for auth_domain in form.cleaned_data["authorization_domains"]: + for auth_domain in form.cleaned_data.get("authorization_domains", []): models.WorkspaceAuthorizationDomain.objects.create(workspace=self.new_workspace, group=auth_domain) workspace_data_formset.forms[0].save() # Then create the workspace on AnVIL. + self.adapter.before_workspace_create(self.new_workspace) authorization_domains = self.new_workspace.authorization_domains.all() self.object.anvil_clone( self.new_workspace.billing_project, self.new_workspace.name, authorization_domains=authorization_domains, ) + self.adapter.after_workspace_create(self.new_workspace) except AnVILAPIError as e: # If the API call failed, rerender the page with the responses and show a message. messages.add_message(self.request, messages.ERROR, "AnVIL API Error: " + str(e)) From 59e9142acfdd0563b2b7e8104108d75ad3f67c68 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 14:32:23 -0700 Subject: [PATCH 4/9] Add an after_workspace_import adapter method This method is run after a workspace is imported. --- .../adapters/workspace.py | 4 ++ .../tests/test_app/adapters.py | 9 ++++ anvil_consortium_manager/tests/test_views.py | 54 +++++++++++++++++++ anvil_consortium_manager/views.py | 1 + 4 files changed, 68 insertions(+) diff --git a/anvil_consortium_manager/adapters/workspace.py b/anvil_consortium_manager/adapters/workspace.py index babce64e..568f1b51 100644 --- a/anvil_consortium_manager/adapters/workspace.py +++ b/anvil_consortium_manager/adapters/workspace.py @@ -157,6 +157,10 @@ def after_workspace_create(self, workspace): """Custom actions to take after a workspace is created on AnVIL.""" pass + def after_workspace_import(self, workspace): + """Custom actions to take after a workspace is imported from AnVIL.""" + pass + class AdapterAlreadyRegisteredError(Exception): """Exception raised when an adapter or its type is already registered.""" diff --git a/anvil_consortium_manager/tests/test_app/adapters.py b/anvil_consortium_manager/tests/test_app/adapters.py index 45b084c7..dc4e79db 100644 --- a/anvil_consortium_manager/tests/test_app/adapters.py +++ b/anvil_consortium_manager/tests/test_app/adapters.py @@ -93,3 +93,12 @@ def after_workspace_create(self, workspace): # Set the extra field to "FOO" workspace.testworkspacemethodsdata.test_field = "FOO" workspace.testworkspacemethodsdata.save() + + +class TestAfterWorkspaceImportAdapter(TestWorkspaceMethodsAdapter): + """Test adapter for workspaces with custom methods defined.""" + + def after_workspace_import(self, workspace): + # Set the extra field. + workspace.testworkspacemethodsdata.test_field = "imported!" + workspace.testworkspacemethodsdata.save() diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py index f494f6ab..ddfd2818 100644 --- a/anvil_consortium_manager/tests/test_views.py +++ b/anvil_consortium_manager/tests/test_views.py @@ -31,6 +31,7 @@ from .test_app import tables as app_tables from .test_app.adapters import ( TestAfterWorkspaceCreateAdapter, + TestAfterWorkspaceImportAdapter, TestBeforeWorkspaceCreateAdapter, TestForeignKeyWorkspaceAdapter, TestWorkspaceAdapter, @@ -9795,6 +9796,59 @@ def test_post_workspace_data_with_second_foreign_key_to_workspace(self): self.assertEqual(new_workspace_data.workspace, new_workspace) self.assertEqual(new_workspace_data.other_workspace, other_workspace) + def test_post_custom_adapter_after_workspace_import(self): + """The after_workspace_create method is run after a workspace is imported.""" + # Overriding settings doesn't work, because appconfig.ready has already run and + # registered the default adapter. Instead, unregister the default and register the + # new adapter here. + workspace_adapter_registry.unregister(DefaultWorkspaceAdapter) + workspace_adapter_registry.register(TestAfterWorkspaceImportAdapter) + self.workspace_type = TestAfterWorkspaceImportAdapter().get_type() + billing_project = factories.BillingProjectFactory.create(name="billing-project") + workspace_name = "workspace" + # Available workspaces API call. + self.anvil_response_mock.add( + responses.GET, + self.workspace_list_url, + match=[ + responses.matchers.query_param_matcher({"fields": "workspace.namespace,workspace.name,accessLevel"}) + ], + status=200, + json=[self.get_api_json_response(billing_project.name, workspace_name)], + ) + # Response for ACL query. + self.anvil_response_mock.add( + responses.GET, + self.get_api_url_acl(billing_project.name, workspace_name), + status=200, # successful response code. + json=self.api_json_response_acl, + ) + url = self.get_api_url(billing_project.name, workspace_name) + self.anvil_response_mock.add( + responses.GET, + url, + status=self.api_success_code, + json=self.get_api_json_response(billing_project.name, workspace_name), + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.workspace_type), + { + "workspace": billing_project.name + "/" + workspace_name, + # Default workspace data for formset. + "workspacedata-TOTAL_FORMS": 1, + "workspacedata-INITIAL_FORMS": 0, + "workspacedata-MIN_NUM_FORMS": 1, + "workspacedata-MAX_NUM_FORMS": 1, + "workspacedata-0-test_field": "my field value", + }, + ) + self.assertEqual(response.status_code, 302) + # The workspace is created. + new_workspace = models.Workspace.objects.latest("pk") + # The test_field field was modified by the adapter. + self.assertEqual(new_workspace.testworkspacemethodsdata.test_field, "imported!") + class WorkspaceCloneTest(AnVILAPIMockTestMixin, TestCase): """Tests for the WorkspaceClone view.""" diff --git a/anvil_consortium_manager/views.py b/anvil_consortium_manager/views.py index 5973ae37..bc2d9db0 100644 --- a/anvil_consortium_manager/views.py +++ b/anvil_consortium_manager/views.py @@ -1204,6 +1204,7 @@ def form_valid(self, form): transaction.set_rollback(True) return self.forms_invalid(form, workspace_data_formset) workspace_data_formset.forms[0].save() + self.adapter.after_workspace_import(self.workspace) except anvil_api.AnVILAPIError as e: messages.add_message(self.request, messages.ERROR, "AnVIL API Error: " + str(e)) return self.render_to_response(self.get_context_data(form=form)) From 45fdf6b51fb601214ebb7144120d1054a0538b5e Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 14:39:25 -0700 Subject: [PATCH 5/9] Remove stray print statements --- anvil_consortium_manager/adapters/workspace.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/anvil_consortium_manager/adapters/workspace.py b/anvil_consortium_manager/adapters/workspace.py index 568f1b51..48b8646e 100644 --- a/anvil_consortium_manager/adapters/workspace.py +++ b/anvil_consortium_manager/adapters/workspace.py @@ -224,8 +224,6 @@ def get_registered_names(self): def populate_from_settings(self): """Populate the workspace adapter registry from settings. Called by AppConfig ready() method.""" adapter_modules = app_settings.WORKSPACE_ADAPTERS - print("adapter modules") - print(adapter_modules) if len(self._registry): msg = "Registry has already been populated." raise RuntimeError(msg) From 7e8f39088e9f218b830668bf0bc98944fe59c471 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 14:41:37 -0700 Subject: [PATCH 6/9] Update documentation for new adapter methods --- docs/advanced.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/advanced.rst b/docs/advanced.rst index 088ea771..c47d9364 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -90,6 +90,9 @@ You may also override default settings and methods: - ``get_autocomplete_queryset``: a method to filter a workspace queryset for use in the :class:`~anvil_consortium_manager.views.WorkspaceAutocompleteByType` view. This queryset passed to this method is the workspace data model specified by the adapter, not the `Workspace` model. - ``get_extra_detail_context_data``: a method to add extra context data to the :class:`~anvil_consortium_manager.views.WorkspaceDetail` view. This method is passed the `Workspace` model, not the workspace data model specified by the adapter. +- ``before_workspace_create``: a method to perform any actions before creating a workspace on AnVIL. +- ``after_workspace_create``: a method to perform any actions after creating a workspace on AnVIL. +- ``after_workspace_import``: a method to perform any actions after importing a workspace from AnVIL. Here is example of the custom adapter for ``my_app`` with the model, form and table defined above. From b41c5d65fce7d97ba5e80f914d5eb4264eb545b9 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 14:42:30 -0700 Subject: [PATCH 7/9] Bump version number and update CHANGELOG --- CHANGELOG.md | 1 + anvil_consortium_manager/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b6b3e89..99603118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Add support for Django 5.0. * Add `convert_mariadb_uuid_fields` command to convert UUID fields for MariaDB 10.7+ and Django 5.0+. See the documentation of this command for more information. * Move app settings to their own file, and set defaults for some settings. +* Add additional customization for WorkspaceAdapters. Users can override the `before_workspace_create`, `after_workspace_create`, and `after_workspace_update` methods to run custom code before or after creating or after importing a workspace. ## 0.23.0 (2024-05-31) diff --git a/anvil_consortium_manager/__init__.py b/anvil_consortium_manager/__init__.py index 7e7cfa29..d0255aed 100644 --- a/anvil_consortium_manager/__init__.py +++ b/anvil_consortium_manager/__init__.py @@ -1 +1 @@ -__version__ = "0.23.1.dev3" +__version__ = "0.24.0.dev0" From 237e18bb998f258af6959245ea2c307d59cedbe2 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 14:46:17 -0700 Subject: [PATCH 8/9] Small modification to docs --- docs/advanced.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index c47d9364..3f04687a 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -90,9 +90,9 @@ You may also override default settings and methods: - ``get_autocomplete_queryset``: a method to filter a workspace queryset for use in the :class:`~anvil_consortium_manager.views.WorkspaceAutocompleteByType` view. This queryset passed to this method is the workspace data model specified by the adapter, not the `Workspace` model. - ``get_extra_detail_context_data``: a method to add extra context data to the :class:`~anvil_consortium_manager.views.WorkspaceDetail` view. This method is passed the `Workspace` model, not the workspace data model specified by the adapter. -- ``before_workspace_create``: a method to perform any actions before creating a workspace on AnVIL. -- ``after_workspace_create``: a method to perform any actions after creating a workspace on AnVIL. -- ``after_workspace_import``: a method to perform any actions after importing a workspace from AnVIL. +- ``before_workspace_create``: a method to perform any actions before creating a workspace on AnVIL via the :class:`~anvil_consortium_manager.views.WorkspaceCreate` view. +- ``after_workspace_create``: a method to perform any actions after creating a workspace on AnVIL via the :class:`~anvil_consortium_manager.views.WorkspaceCreate` view. +- ``after_workspace_import``: a method to perform any actions after importing a workspace from AnVIL via the :class:`~anvil_consortium_manager.views.WorkspaceImport` view. Here is example of the custom adapter for ``my_app`` with the model, form and table defined above. From cd6ea592f7b5b324fc3dad84b1dfb48eebb26456 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Tue, 2 Jul 2024 16:35:34 -0700 Subject: [PATCH 9/9] Rename adapter methods from workspace to anvil before_anvil_create, after_anvil_create, after_anvil_import --- CHANGELOG.md | 2 +- .../adapters/workspace.py | 6 +++--- .../tests/test_app/adapters.py | 6 +++--- anvil_consortium_manager/tests/test_views.py | 20 +++++++++---------- anvil_consortium_manager/views.py | 10 +++++----- docs/advanced.rst | 6 +++--- example_site/app/adapters.py | 2 +- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99603118..4b531107 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * Add support for Django 5.0. * Add `convert_mariadb_uuid_fields` command to convert UUID fields for MariaDB 10.7+ and Django 5.0+. See the documentation of this command for more information. * Move app settings to their own file, and set defaults for some settings. -* Add additional customization for WorkspaceAdapters. Users can override the `before_workspace_create`, `after_workspace_create`, and `after_workspace_update` methods to run custom code before or after creating or after importing a workspace. +* Add additional customization for WorkspaceAdapters. Users can override the `before_anvil_create`, `after_anvil_create`, and `after_anvil_import` methods to run custom code before or after creating or after importing a workspace. ## 0.23.0 (2024-05-31) diff --git a/anvil_consortium_manager/adapters/workspace.py b/anvil_consortium_manager/adapters/workspace.py index 48b8646e..f6e6e669 100644 --- a/anvil_consortium_manager/adapters/workspace.py +++ b/anvil_consortium_manager/adapters/workspace.py @@ -149,15 +149,15 @@ def get_extra_detail_context_data(self, workspace, request): return {} - def before_workspace_create(self, workspace): + def before_anvil_create(self, workspace): """Custom actions to take after a workspace is created on AnVIL.""" pass - def after_workspace_create(self, workspace): + def after_anvil_create(self, workspace): """Custom actions to take after a workspace is created on AnVIL.""" pass - def after_workspace_import(self, workspace): + def after_anvil_import(self, workspace): """Custom actions to take after a workspace is imported from AnVIL.""" pass diff --git a/anvil_consortium_manager/tests/test_app/adapters.py b/anvil_consortium_manager/tests/test_app/adapters.py index dc4e79db..92883e1b 100644 --- a/anvil_consortium_manager/tests/test_app/adapters.py +++ b/anvil_consortium_manager/tests/test_app/adapters.py @@ -80,7 +80,7 @@ class TestWorkspaceMethodsAdapter(BaseWorkspaceAdapter): class TestBeforeWorkspaceCreateAdapter(TestWorkspaceMethodsAdapter): """Test adapter for workspaces with custom methods defined.""" - def before_workspace_create(self, workspace): + def before_anvil_create(self, workspace): # Append a -2 to the name of the workspace. workspace.name = workspace.name + "-2" workspace.save() @@ -89,7 +89,7 @@ def before_workspace_create(self, workspace): class TestAfterWorkspaceCreateAdapter(TestWorkspaceMethodsAdapter): """Test adapter for workspaces with custom methods defined.""" - def after_workspace_create(self, workspace): + def after_anvil_create(self, workspace): # Set the extra field to "FOO" workspace.testworkspacemethodsdata.test_field = "FOO" workspace.testworkspacemethodsdata.save() @@ -98,7 +98,7 @@ def after_workspace_create(self, workspace): class TestAfterWorkspaceImportAdapter(TestWorkspaceMethodsAdapter): """Test adapter for workspaces with custom methods defined.""" - def after_workspace_import(self, workspace): + def after_anvil_import(self, workspace): # Set the extra field. workspace.testworkspacemethodsdata.test_field = "imported!" workspace.testworkspacemethodsdata.save() diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py index ddfd2818..ea928669 100644 --- a/anvil_consortium_manager/tests/test_views.py +++ b/anvil_consortium_manager/tests/test_views.py @@ -8279,8 +8279,8 @@ def test_post_workspace_data_with_second_foreign_key_to_workspace(self): self.assertEqual(new_workspace_data.workspace, new_workspace) self.assertEqual(new_workspace_data.other_workspace, other_workspace) - def test_post_custom_adapter_before_workspace_create(self): - """The before_workspace_create method is run before a workspace is created.""" + def test_post_custom_adapter_before_anvil_create(self): + """The before_anvil_create method is run before a workspace is created.""" # Overriding settings doesn't work, because appconfig.ready has already run and # registered the default adapter. Instead, unregister the default and register the # new adapter here. @@ -8317,8 +8317,8 @@ def test_post_custom_adapter_before_workspace_create(self): new_workspace = models.Workspace.objects.latest("pk") self.assertEqual(new_workspace.name, "test-workspace-2") - def test_post_custom_adapter_after_workspace_create(self): - """The after_workspace_create method is run after a workspace is created.""" + def test_post_custom_adapter_after_anvil_create(self): + """The after_anvil_create method is run after a workspace is created.""" # Overriding settings doesn't work, because appconfig.ready has already run and # registered the default adapter. Instead, unregister the default and register the # new adapter here. @@ -9796,8 +9796,8 @@ def test_post_workspace_data_with_second_foreign_key_to_workspace(self): self.assertEqual(new_workspace_data.workspace, new_workspace) self.assertEqual(new_workspace_data.other_workspace, other_workspace) - def test_post_custom_adapter_after_workspace_import(self): - """The after_workspace_create method is run after a workspace is imported.""" + def test_post_custom_adapter_after_anvil_import(self): + """The after_anvil_create method is run after a workspace is imported.""" # Overriding settings doesn't work, because appconfig.ready has already run and # registered the default adapter. Instead, unregister the default and register the # new adapter here. @@ -10975,8 +10975,8 @@ def test_post_workspace_data_with_second_foreign_key_to_workspace(self): self.assertEqual(new_workspace_data.workspace, new_workspace) self.assertEqual(new_workspace_data.other_workspace, other_workspace) - def test_post_custom_adapter_before_workspace_create(self): - """The before_workspace_create method is run before a workspace is created.""" + def test_post_custom_adapter_before_anvil_create(self): + """The before_anvil_create method is run before a workspace is created.""" # Overriding settings doesn't work, because appconfig.ready has already run and # registered the default adapter. Instead, unregister the default and register the # new adapter here. @@ -11018,8 +11018,8 @@ def test_post_custom_adapter_before_workspace_create(self): new_workspace = models.Workspace.objects.latest("pk") self.assertEqual(new_workspace.name, "test-workspace-2") - def test_post_custom_adapter_after_workspace_create(self): - """The after_workspace_create method is run after a workspace is created.""" + def test_post_custom_adapter_after_anvil_create(self): + """The after_anvil_create method is run after a workspace is created.""" # Overriding settings doesn't work, because appconfig.ready has already run and # registered the default adapter. Instead, unregister the default and register the # new adapter here. diff --git a/anvil_consortium_manager/views.py b/anvil_consortium_manager/views.py index bc2d9db0..66bfc190 100644 --- a/anvil_consortium_manager/views.py +++ b/anvil_consortium_manager/views.py @@ -1064,9 +1064,9 @@ def form_valid(self, form): models.WorkspaceAuthorizationDomain.objects.create(workspace=self.workspace, group=auth_domain) workspace_data_formset.forms[0].save() # Then create the workspace on AnVIL, running custom adapter methods as appropriate. - self.adapter.before_workspace_create(self.workspace) + self.adapter.before_anvil_create(self.workspace) self.workspace.anvil_create() - self.adapter.after_workspace_create(self.workspace) + self.adapter.after_anvil_create(self.workspace) except AnVILAPIError as e: # If the API call failed, rerender the page with the responses and show a message. messages.add_message(self.request, messages.ERROR, "AnVIL API Error: " + str(e)) @@ -1204,7 +1204,7 @@ def form_valid(self, form): transaction.set_rollback(True) return self.forms_invalid(form, workspace_data_formset) workspace_data_formset.forms[0].save() - self.adapter.after_workspace_import(self.workspace) + self.adapter.after_anvil_import(self.workspace) except anvil_api.AnVILAPIError as e: messages.add_message(self.request, messages.ERROR, "AnVIL API Error: " + str(e)) return self.render_to_response(self.get_context_data(form=form)) @@ -1332,14 +1332,14 @@ def form_valid(self, form): models.WorkspaceAuthorizationDomain.objects.create(workspace=self.new_workspace, group=auth_domain) workspace_data_formset.forms[0].save() # Then create the workspace on AnVIL. - self.adapter.before_workspace_create(self.new_workspace) + self.adapter.before_anvil_create(self.new_workspace) authorization_domains = self.new_workspace.authorization_domains.all() self.object.anvil_clone( self.new_workspace.billing_project, self.new_workspace.name, authorization_domains=authorization_domains, ) - self.adapter.after_workspace_create(self.new_workspace) + self.adapter.after_anvil_create(self.new_workspace) except AnVILAPIError as e: # If the API call failed, rerender the page with the responses and show a message. messages.add_message(self.request, messages.ERROR, "AnVIL API Error: " + str(e)) diff --git a/docs/advanced.rst b/docs/advanced.rst index 3f04687a..e165ed3e 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -90,9 +90,9 @@ You may also override default settings and methods: - ``get_autocomplete_queryset``: a method to filter a workspace queryset for use in the :class:`~anvil_consortium_manager.views.WorkspaceAutocompleteByType` view. This queryset passed to this method is the workspace data model specified by the adapter, not the `Workspace` model. - ``get_extra_detail_context_data``: a method to add extra context data to the :class:`~anvil_consortium_manager.views.WorkspaceDetail` view. This method is passed the `Workspace` model, not the workspace data model specified by the adapter. -- ``before_workspace_create``: a method to perform any actions before creating a workspace on AnVIL via the :class:`~anvil_consortium_manager.views.WorkspaceCreate` view. -- ``after_workspace_create``: a method to perform any actions after creating a workspace on AnVIL via the :class:`~anvil_consortium_manager.views.WorkspaceCreate` view. -- ``after_workspace_import``: a method to perform any actions after importing a workspace from AnVIL via the :class:`~anvil_consortium_manager.views.WorkspaceImport` view. +- ``before_anvil_create``: a method to perform any actions before creating a workspace on AnVIL via the :class:`~anvil_consortium_manager.views.WorkspaceCreate` view. +- ``after_anvil_create``: a method to perform any actions after creating a workspace on AnVIL via the :class:`~anvil_consortium_manager.views.WorkspaceCreate` view. +- ``after_anvil_import``: a method to perform any actions after importing a workspace from AnVIL via the :class:`~anvil_consortium_manager.views.WorkspaceImport` view. Here is example of the custom adapter for ``my_app`` with the model, form and table defined above. diff --git a/example_site/app/adapters.py b/example_site/app/adapters.py index dbfcb034..6faa5a25 100644 --- a/example_site/app/adapters.py +++ b/example_site/app/adapters.py @@ -17,7 +17,7 @@ class CustomWorkspaceAdapter(BaseWorkspaceAdapter): workspace_data_form_class = forms.CustomWorkspaceDataForm workspace_detail_template_name = "app/custom_workspace_detail.html" - def before_workspace_create(self, workspace): + def before_anvil_create(self, workspace): """Add authorization domain to workspace.""" auth_domain_name = "AUTH_" + workspace.name auth_domain = ManagedGroup.objects.create(