From 0a5f81ad51307002ce4279ec462e0b1fc645b7fc Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 09:46:30 -0700 Subject: [PATCH 01/13] Move app-specific settings into their own file This file can check that required settings are set, and use defaults for other settings. --- anvil_consortium_manager/app_settings.py | 41 ++++++++++++++++++++++++ example_site/settings.py | 20 +++++++----- 2 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 anvil_consortium_manager/app_settings.py diff --git a/anvil_consortium_manager/app_settings.py b/anvil_consortium_manager/app_settings.py new file mode 100644 index 00000000..ddec1876 --- /dev/null +++ b/anvil_consortium_manager/app_settings.py @@ -0,0 +1,41 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +# The following settings are used to configure the Anvil Consortium Manager. +# Users should set or override these values in their Django project's settings.py file. +# This file provides defaults for some of the settings. + +# Required settings +# ----------------- + +# The path to the service account to use for managing access on AnVIL. +try: + ANVIL_API_SERVICE_ACCOUNT_FILE = getattr(settings, "ANVIL_API_SERVICE_ACCOUNT_FILE") +except AttributeError: + raise ImproperlyConfigured("ANVIL_API_SERVICE_ACCOUNT_FILE is required in settings.py") + +# Workspace adapters. +try: + ANVIL_WORKSPACE_ADAPTERS = getattr(settings, "ANVIL_WORKSPACE_ADAPTERS") +except AttributeError: + raise ImproperlyConfigured("ANVIL_WORKSPACE_ADAPTERS is required in settings.py") + +# Optional settings +# ----------------- + +# Subject line for AnVIL account verification emails. +ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = getattr( + settings, "ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT", "Verify your AnVIL account email" +) + +# The URL for AccountLinkVerify view redirect +ANVIL_ACCOUNT_LINK_REDIRECT = getattr(settings, "ANVIL_ACCOUNT_LINK_REDIRECT", settings.LOGIN_REDIRECT_URL) + +# If desired, specify the email address to send an email to after a user verifies an account. +# Set to None to disable (default). +ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL = getattr(settings, "ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL", None) + +# Account adapter. +ANVIL_ACCOUNT_ADAPTER = getattr( + settings, "ANVIL_ACCOUNT_ADAPTER", "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" +) diff --git a/example_site/settings.py b/example_site/settings.py index 5c4a08f7..b2552e62 100644 --- a/example_site/settings.py +++ b/example_site/settings.py @@ -213,17 +213,21 @@ # ------------------------------------------------------------------------------ # Specify the path to the service account to use for managing access on AnVIL. ANVIL_API_SERVICE_ACCOUNT_FILE = env("ANVIL_API_SERVICE_ACCOUNT_FILE") -# Specify the URL for AccountLinkVerify view redirect -ANVIL_ACCOUNT_LINK_REDIRECT = "home" -# Specify the subject for AnVIL account verification emails. -ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = "Verify your AnVIL account email" -# If desired, specify the email address to send an email to after a user verifies an account. -# ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL = "to@example.com" -# Workspace adapters. +# Specify the workspace adapters to use. ANVIL_WORKSPACE_ADAPTERS = [ "anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter", "example_site.app.adapters.CustomWorkspaceAdapter", ] + +# Specify the URL for AccountLinkVerify view redirect +# ANVIL_ACCOUNT_LINK_REDIRECT = LOGIN_REDIRECT_URL # Default + +# Specify the subject for AnVIL account verification emails. +# ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = "Verify your AnVIL account email" # Default. + +# If desired, specify the email address to send an email to after a user verifies an account. +# ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL = "to@example.com" + # Account adapter. -ANVIL_ACCOUNT_ADAPTER = "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" +# ANVIL_ACCOUNT_ADAPTER = "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" # Default. From 023c325c7b64cb2c03de887c8d658248c37226a7 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 10:26:49 -0700 Subject: [PATCH 02/13] Use a class to handle app settings This allows for more dynamic changing of settings, eg in tests. --- anvil_consortium_manager/app_settings.py | 112 ++++++++++++++++++----- 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/anvil_consortium_manager/app_settings.py b/anvil_consortium_manager/app_settings.py index ddec1876..a8cae22e 100644 --- a/anvil_consortium_manager/app_settings.py +++ b/anvil_consortium_manager/app_settings.py @@ -1,6 +1,68 @@ +# App settings. +# Mostly follows django-allauth: +# https://github.com/pennersr/django-allauth/blob/main/allauth/app_settings.py + from django.conf import settings from django.core.exceptions import ImproperlyConfigured + +class AppSettings(object): + """Class to handle settings for django-anvil-consortium-manager.""" + + def __init__(self, prefix): + self.prefix = prefix + + def _setting(self, name, default=None): + from django.conf import settings + + return getattr(settings, self.prefix + name, default) + + @property + def API_SERVICE_ACCOUNT_FILE(self): + """The path to the service account to use for managing access on AnVIL. Required.""" + x = self._setting("API_SERVICE_ACCOUNT_FILE") + if not x: + raise ImproperlyConfigured("ANVIL_API_SERVICE_ACCOUNT_FILE is required in settings.py") + + @property + def WORKSPACE_ADAPTERS(self): + """Workspace adapters. Required.""" + x = self._setting("WORKSPACE_ADAPTERS") + if not x: + raise ImproperlyConfigured("ANVIL_WORKSPACE_ADAPTERS is required in settings.py") + + @property + def ACCOUNT_LINK_EMAIL_SUBJECT(self): + """Subject line for AnVIL account verification emails. Default: 'Verify your AnVIL account email'""" + return self._setting("ACCOUNT_LINK_EMAIL_SUBJECT", "Verify your AnVIL account email") + + @property + def ACCOUNT_LINK_REDIRECT(self): + """The URL for AccountLinkVerify view redirect. Default: settings.LOGIN_REDIRECT_URL.""" + return self._setting("ACCOUNT_LINK_REDIRECT", settings.LOGIN_REDIRECT_URL) + + @property + def ACCOUNT_VERIFY_NOTIFICATION_EMAIL(self): + """If desired, specify the email address to send an email to after a user verifies an account. Default: None. + + Set to None to disable (default). + """ + return self._setting("ACCOUNT_VERIFY_NOTIFICATION_EMAIL", None) + + @property + def ACCOUNT_ADAPTER(self): + """Account adapter. Default: anvil_consortium_manager.adapters.default.DefaultAccountAdapter.""" + return self._setting("ACCOUNT_ADAPTER", "anvil_consortium_manager.adapters.default.DefaultAccountAdapter") + + +_app_settings = AppSettings("ANVIL_") + + +def __getattr__(name): + # See https://peps.python.org/pep-0562/ + return getattr(_app_settings, name) + + # The following settings are used to configure the Anvil Consortium Manager. # Users should set or override these values in their Django project's settings.py file. # This file provides defaults for some of the settings. @@ -8,34 +70,34 @@ # Required settings # ----------------- -# The path to the service account to use for managing access on AnVIL. -try: - ANVIL_API_SERVICE_ACCOUNT_FILE = getattr(settings, "ANVIL_API_SERVICE_ACCOUNT_FILE") -except AttributeError: - raise ImproperlyConfigured("ANVIL_API_SERVICE_ACCOUNT_FILE is required in settings.py") +# # The path to the service account to use for managing access on AnVIL. +# try: +# ANVIL_API_SERVICE_ACCOUNT_FILE = getattr(settings, "ANVIL_API_SERVICE_ACCOUNT_FILE") +# except AttributeError: +# raise ImproperlyConfigured("ANVIL_API_SERVICE_ACCOUNT_FILE is required in settings.py") -# Workspace adapters. -try: - ANVIL_WORKSPACE_ADAPTERS = getattr(settings, "ANVIL_WORKSPACE_ADAPTERS") -except AttributeError: - raise ImproperlyConfigured("ANVIL_WORKSPACE_ADAPTERS is required in settings.py") +# # Workspace adapters. +# try: +# ANVIL_WORKSPACE_ADAPTERS = getattr(settings, "ANVIL_WORKSPACE_ADAPTERS") +# except AttributeError: +# raise ImproperlyConfigured("ANVIL_WORKSPACE_ADAPTERS is required in settings.py") -# Optional settings -# ----------------- +# # Optional settings +# # ----------------- -# Subject line for AnVIL account verification emails. -ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = getattr( - settings, "ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT", "Verify your AnVIL account email" -) +# # Subject line for AnVIL account verification emails. +# ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = getattr( +# settings, "ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT", "Verify your AnVIL account email" +# ) -# The URL for AccountLinkVerify view redirect -ANVIL_ACCOUNT_LINK_REDIRECT = getattr(settings, "ANVIL_ACCOUNT_LINK_REDIRECT", settings.LOGIN_REDIRECT_URL) +# # The URL for AccountLinkVerify view redirect +# ANVIL_ACCOUNT_LINK_REDIRECT = getattr(settings, "ANVIL_ACCOUNT_LINK_REDIRECT", settings.LOGIN_REDIRECT_URL) -# If desired, specify the email address to send an email to after a user verifies an account. -# Set to None to disable (default). -ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL = getattr(settings, "ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL", None) +# # If desired, specify the email address to send an email to after a user verifies an account. +# # Set to None to disable (default). +# ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL = getattr(settings, "ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL", None) -# Account adapter. -ANVIL_ACCOUNT_ADAPTER = getattr( - settings, "ANVIL_ACCOUNT_ADAPTER", "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" -) +# # Account adapter. +# ANVIL_ACCOUNT_ADAPTER = getattr( +# settings, "ANVIL_ACCOUNT_ADAPTER", "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" +# ) From 706ba11a605b504045c443667580b78855907e3d Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 10:59:32 -0700 Subject: [PATCH 03/13] Update code and tests for ANVIL_ACCOUNT_LINK_REDIRECT setting Use the new app_settings module to get the value for this setting. --- anvil_consortium_manager/adapters/account.py | 5 +- .../tests/settings/test.py | 5 +- anvil_consortium_manager/tests/test_views.py | 49 ++++++++++++++----- anvil_consortium_manager/views.py | 15 +++--- 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/anvil_consortium_manager/adapters/account.py b/anvil_consortium_manager/adapters/account.py index 3017d44c..bbf0df1f 100644 --- a/anvil_consortium_manager/adapters/account.py +++ b/anvil_consortium_manager/adapters/account.py @@ -2,12 +2,11 @@ from abc import ABC, abstractproperty -from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils.module_loading import import_string from django_filters import FilterSet -from .. import models +from .. import app_settings, models class BaseAccountAdapter(ABC): @@ -53,5 +52,5 @@ def get_autocomplete_label(self, account): def get_account_adapter(): - adapter = import_string(settings.ANVIL_ACCOUNT_ADAPTER) + adapter = import_string(app_settings.ACCOUNT_ADAPTER) return adapter diff --git a/anvil_consortium_manager/tests/settings/test.py b/anvil_consortium_manager/tests/settings/test.py index 77a6d8d6..0ec880ca 100644 --- a/anvil_consortium_manager/tests/settings/test.py +++ b/anvil_consortium_manager/tests/settings/test.py @@ -126,6 +126,7 @@ # Since there are no templates for redirects in this app, specify the open URL. LOGIN_URL = "test_login" +LOGIN_REDIRECT_URL = "test_home" # Django is switching how forms are handled (divs). Set the FORM_RENDERER temporary setting until # it is removed in Django 6.0. @@ -147,10 +148,10 @@ # Path to the service account to use for managing access. # Because the calls are mocked, we don't need to set this. ANVIL_API_SERVICE_ACCOUNT_FILE = "foo" -ANVIL_ACCOUNT_LINK_REDIRECT = "test_home" +# ANVIL_ACCOUNT_LINK_REDIRECT = "test_home" ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = "account activation" ANVIL_WORKSPACE_ADAPTERS = [ "anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter", ] -ANVIL_ACCOUNT_ADAPTER = "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" +# ANVIL_ACCOUNT_ADAPTER = "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py index b905c0ac..70705d94 100644 --- a/anvil_consortium_manager/tests/test_views.py +++ b/anvil_consortium_manager/tests/test_views.py @@ -2122,7 +2122,19 @@ def test_redirect(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.post(self.get_url(), {"email": email}) - self.assertRedirects(response, "/test_home/") + self.assertRedirects(response, reverse(settings.LOGIN_REDIRECT_URL)) + + @override_settings(ANVIL_ACCOUNT_LINK_REDIRECT="test_login") + def test_redirect_custom(self): + """View redirects to the correct URL.""" + email = "test@example.com" + api_url = self.get_api_url(email) + self.anvil_response_mock.add(responses.GET, api_url, status=200, json=self.get_api_json_response(email)) + # Need a client because messages are added. + self.client.force_login(self.user) + response = self.client.post(self.get_url(), {"email": email}) + # import ipdb; ipdb.set_trace() + self.assertRedirects(response, reverse("test_login")) # This test occasionally fails if the time flips one second between sending the email and # regenerating the token. Use freezegun's freeze_time decorator to fix the time and avoid @@ -2251,7 +2263,7 @@ def test_account_already_linked_to_different_user_and_verified(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.post(self.get_url(), {"email": email}, follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No new UserEmailEntry is created. self.assertEqual(models.UserEmailEntry.objects.count(), 1) self.assertEqual(models.UserEmailEntry.objects.latest("pk"), other_email_entry) @@ -2315,7 +2327,7 @@ def test_account_exists_with_email_but_not_linked_to_user(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.post(self.get_url(), {"email": email}, follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No new UserEmailEntry is created. self.assertEqual(models.UserEmailEntry.objects.count(), 0) # No email is sent. @@ -2585,7 +2597,7 @@ def test_user_with_perms_can_verify_email(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.get(self.get_url(email_entry.uuid, token), follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # A new account is created. self.assertEqual(models.Account.objects.count(), 1) new_object = models.Account.objects.latest("pk") @@ -2607,13 +2619,26 @@ def test_user_with_perms_can_verify_email(self): self.assertEqual(len(messages), 1) self.assertEqual(str(messages[0]), views.AccountLinkVerify.message_success) + @override_settings(ANVIL_ACCOUNT_LINK_REDIRECT="test_login") + def test_custom_redirect(self): + """A user can successfully verify their email.""" + email = "test@example.com" + email_entry = factories.UserEmailEntryFactory.create(user=self.user, email=email) + token = account_verification_token.make_token(email_entry) + api_url = self.get_api_url(email) + self.anvil_response_mock.add(responses.GET, api_url, status=200, json=self.get_api_json_response(email)) + # Need a client because messages are added. + self.client.force_login(self.user) + response = self.client.get(self.get_url(email_entry.uuid, token), follow=True) + self.assertRedirects(response, "/test_login/") + def test_user_email_entry_does_not_exist(self): """ "There is no UserEmailEntry with the uuid.""" token = "foo" # Need a client because messages are added. self.client.force_login(self.user) response = self.client.get(self.get_url(uuid4(), token), follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No new accounts are created. self.assertEqual(models.Account.objects.count(), 0) # No UserEmailEntry objects exist. @@ -2633,7 +2658,7 @@ def test_this_user_already_verified_this_email(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.get(self.get_url(email_entry.uuid, token), follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No new accounts are created. self.assertEqual(models.Account.objects.count(), 1) self.assertEqual(account, models.Account.objects.latest("pk")) @@ -2655,7 +2680,7 @@ def test_user_already_verified_different_email(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.get(self.get_url(email_entry.uuid, token), follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No new accounts are created. self.assertEqual(models.Account.objects.count(), 1) self.assertEqual(existing_account, models.Account.objects.latest("pk")) @@ -2676,7 +2701,7 @@ def test_token_does_not_match(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.get(self.get_url(email_entry.uuid, token), follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No accounts are created. self.assertEqual(models.Account.objects.count(), 0) # The email entry objects are not changed -- no history is added. @@ -2700,7 +2725,7 @@ def test_different_user_verified_this_email(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.get(self.get_url(email_entry.uuid, token), follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No new accounts are created. self.assertEqual(models.Account.objects.count(), 1) self.assertEqual(other_account, models.Account.objects.latest("pk")) @@ -2726,7 +2751,7 @@ def test_anvil_account_no_longer_exists(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.get(self.get_url(email_entry.uuid, token), follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No accounts are created. self.assertEqual(models.Account.objects.count(), 0) # The email_entry object was not updated. @@ -2749,7 +2774,7 @@ def test_email_associated_with_group(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.get(self.get_url(email_entry.uuid, token), follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No accounts are created. self.assertEqual(models.Account.objects.count(), 0) # The email_entry object was not updated. @@ -2772,7 +2797,7 @@ def test_api_call_fails(self): # Need a client because messages are added. self.client.force_login(self.user) response = self.client.get(self.get_url(email_entry.uuid, token), follow=True) - self.assertRedirects(response, reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + self.assertRedirects(response, "/test_home/") # No accounts are created. self.assertEqual(models.Account.objects.count(), 0) # The email_entry object was not updated. diff --git a/anvil_consortium_manager/views.py b/anvil_consortium_manager/views.py index 2facaa2b..a1f6376b 100644 --- a/anvil_consortium_manager/views.py +++ b/anvil_consortium_manager/views.py @@ -15,7 +15,7 @@ from django_filters.views import FilterView from django_tables2 import SingleTableMixin, SingleTableView -from . import __version__, anvil_api, auth, exceptions, filters, forms, models, tables, viewmixins +from . import __version__, anvil_api, app_settings, auth, exceptions, filters, forms, models, tables, viewmixins from .adapters.account import get_account_adapter from .adapters.workspace import workspace_adapter_registry from .anvil_api import AnVILAPIClient, AnVILAPIError @@ -244,6 +244,9 @@ class AccountLink(auth.AnVILConsortiumManagerAccountLinkRequired, SuccessMessage form_class = forms.UserEmailEntryForm success_message = "To complete linking the account, check your email for a verification link." + def get_redirect_url(self): + return reverse(app_settings.ACCOUNT_LINK_REDIRECT) + def get(self, request, *args, **kwargs): """Check if the user already has an account linked and redirect.""" try: @@ -253,7 +256,7 @@ def get(self, request, *args, **kwargs): else: # The user already has a linked account, so redirect with a message. messages.add_message(self.request, messages.ERROR, self.message_user_already_linked) - return HttpResponseRedirect(reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + return HttpResponseRedirect(self.get_redirect_url()) def post(self, request, *args, **kwargs): """Check if the user already has an account linked and redirect.""" @@ -264,10 +267,10 @@ def post(self, request, *args, **kwargs): else: # The user already has a linked account, so redirect with a message. messages.add_message(self.request, messages.ERROR, self.message_user_already_linked) - return HttpResponseRedirect(reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + return HttpResponseRedirect(self.get_redirect_url()) def get_success_url(self): - return reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT) + return self.get_redirect_url() def form_valid(self, form): """If the form is valid, check that the email exists on AnVIL and send verification email.""" @@ -283,7 +286,7 @@ def form_valid(self, form): if models.Account.objects.filter(email=email).count(): # The user already has a linked account, so redirect with a message. messages.add_message(self.request, messages.ERROR, self.message_account_already_exists) - return HttpResponseRedirect(reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT)) + return HttpResponseRedirect(self.get_redirect_url()) # Check if it exists on AnVIL. try: @@ -314,7 +317,7 @@ class AccountLinkVerify(auth.AnVILConsortiumManagerAccountLinkRequired, Redirect message_success = "Thank you for verifying your email." def get_redirect_url(self, *args, **kwargs): - return reverse(settings.ANVIL_ACCOUNT_LINK_REDIRECT) + return reverse(app_settings.ACCOUNT_LINK_REDIRECT) def get(self, request, *args, **kwargs): # Check if this user already has an account linked. From ef45e0a6a7b04199f7e83f926625b47a9303fad1 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 11:21:01 -0700 Subject: [PATCH 04/13] Update code and tests for ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT setting --- anvil_consortium_manager/models.py | 4 ++-- .../tests/settings/test.py | 2 +- anvil_consortium_manager/tests/test_models.py | 22 ++++++++++++++++++- anvil_consortium_manager/tests/test_views.py | 19 +++++++++++++--- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/anvil_consortium_manager/models.py b/anvil_consortium_manager/models.py index 19be2c9b..ab7f5a2a 100644 --- a/anvil_consortium_manager/models.py +++ b/anvil_consortium_manager/models.py @@ -11,7 +11,7 @@ from django_extensions.db.models import ActivatorModel, TimeStampedModel from simple_history.models import HistoricalRecords, HistoricForeignKey -from . import exceptions +from . import app_settings, exceptions from .adapters.workspace import workspace_adapter_registry from .anvil_api import AnVILAPIClient, AnVILAPIError, AnVILAPIError404 from .tokens import account_verification_token @@ -142,7 +142,7 @@ def send_verification_email(self, domain): Args: domain (str): The domain of the current site, used to create the link. """ - mail_subject = settings.ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT + mail_subject = app_settings.ACCOUNT_LINK_EMAIL_SUBJECT url_subdirectory = "http://{domain}{url}".format( domain=domain, url=reverse( diff --git a/anvil_consortium_manager/tests/settings/test.py b/anvil_consortium_manager/tests/settings/test.py index 0ec880ca..a3651656 100644 --- a/anvil_consortium_manager/tests/settings/test.py +++ b/anvil_consortium_manager/tests/settings/test.py @@ -149,7 +149,7 @@ # Because the calls are mocked, we don't need to set this. ANVIL_API_SERVICE_ACCOUNT_FILE = "foo" # ANVIL_ACCOUNT_LINK_REDIRECT = "test_home" -ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = "account activation" +# ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = "account activation" ANVIL_WORKSPACE_ADAPTERS = [ "anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter", diff --git a/anvil_consortium_manager/tests/test_models.py b/anvil_consortium_manager/tests/test_models.py index 517df634..d4109f66 100644 --- a/anvil_consortium_manager/tests/test_models.py +++ b/anvil_consortium_manager/tests/test_models.py @@ -173,7 +173,27 @@ def test_send_verification_email(self): # One message has been sent. self.assertEqual(len(mail.outbox), 1) # The subject is correct. - self.assertEqual(mail.outbox[0].subject, "account activation") + self.assertEqual(mail.outbox[0].subject, "Verify your AnVIL account email") + # The contents are correct. + email_body = mail.outbox[0].body + self.assertIn("http://www.test.com", email_body) + self.assertIn(email_entry.user.username, email_body) + self.assertIn(account_verification_token.make_token(email_entry), email_body) + self.assertIn(str(email_entry.uuid), email_body) + + # This test occasionally fails if the time flips one second between sending the email and + # regenerating the token. Use freezegun's freeze_time decorator to fix the time and avoid + # this spurious failure. + @freeze_time("2022-11-22 03:12:34") + @override_settings(ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT="custom subject") + def test_send_verification_email_custom_subject(self): + """Verification email is correct.""" + email_entry = factories.UserEmailEntryFactory.create() + email_entry.send_verification_email("www.test.com") + # One message has been sent. + self.assertEqual(len(mail.outbox), 1) + # The subject is correct. + self.assertEqual(mail.outbox[0].subject, "custom subject") # The contents are correct. email_body = mail.outbox[0].body self.assertIn("http://www.test.com", email_body) diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py index 70705d94..1d33d104 100644 --- a/anvil_consortium_manager/tests/test_views.py +++ b/anvil_consortium_manager/tests/test_views.py @@ -2152,7 +2152,7 @@ def test_email_is_sent(self): # One message has been sent. self.assertEqual(len(mail.outbox), 1) # The subject is correct. - self.assertEqual(mail.outbox[0].subject, "account activation") + self.assertEqual(mail.outbox[0].subject, "Verify your AnVIL account email") url = "http://example.com" + reverse( "anvil_consortium_manager:accounts:verify", args=[email_entry.uuid, account_verification_token.make_token(email_entry)], @@ -2160,6 +2160,21 @@ def test_email_is_sent(self): # The body contains the correct url. self.assertIn(url, mail.outbox[0].body) + @freeze_time("2022-11-22 03:12:34") + @override_settings(ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT="custom subject") + def test_email_is_sent_custom_subject(self): + """An email is sent when the form is submitted correctly.""" + email = "test@example.com" + api_url = self.get_api_url(email) + self.anvil_response_mock.add(responses.GET, api_url, status=200, json=self.get_api_json_response(email)) + # Need a client because messages are added. + self.client.force_login(self.user) + self.client.post(self.get_url(), {"email": email}) + # One message has been sent. + self.assertEqual(len(mail.outbox), 1) + # The subject is correct. + self.assertEqual(mail.outbox[0].subject, "custom subject") + @freeze_time("2022-11-22 03:12:34") def test_email_is_sent_site_domain(self): """An email is sent when the form is submitted correctly.""" @@ -2175,8 +2190,6 @@ def test_email_is_sent_site_domain(self): email_entry = models.UserEmailEntry.objects.get(email=email) # One message has been sent. self.assertEqual(len(mail.outbox), 1) - # The subject is correct. - self.assertEqual(mail.outbox[0].subject, "account activation") url = "http://foobar.com" + reverse( "anvil_consortium_manager:accounts:verify", args=[email_entry.uuid, account_verification_token.make_token(email_entry)], From 95b9faa96ff7b17c37a61463340725affb4a2fb2 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 11:24:21 -0700 Subject: [PATCH 05/13] Update code and tests for ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL --- anvil_consortium_manager/models.py | 8 ++------ anvil_consortium_manager/tests/test_models.py | 7 ------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/anvil_consortium_manager/models.py b/anvil_consortium_manager/models.py index ab7f5a2a..e2ea1a26 100644 --- a/anvil_consortium_manager/models.py +++ b/anvil_consortium_manager/models.py @@ -161,11 +161,7 @@ def send_verification_email(self, domain): def send_notification_email(self): """Send notification email after account is verified if the email setting is set""" - if ( - hasattr(settings, "ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL") - and settings.ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL - and not settings.ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL.isspace() - ): + if app_settings.ACCOUNT_VERIFY_NOTIFICATION_EMAIL: mail_subject = "User verified AnVIL account" message = render_to_string( "anvil_consortium_manager/account_notification_email.html", @@ -178,7 +174,7 @@ def send_notification_email(self): mail_subject, message, None, - [settings.ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL], + [app_settings.ACCOUNT_VERIFY_NOTIFICATION_EMAIL], fail_silently=False, ) diff --git a/anvil_consortium_manager/tests/test_models.py b/anvil_consortium_manager/tests/test_models.py index d4109f66..96bb5c85 100644 --- a/anvil_consortium_manager/tests/test_models.py +++ b/anvil_consortium_manager/tests/test_models.py @@ -221,13 +221,6 @@ def test_send_notification_email_none(self): email_entry.send_notification_email() self.assertEqual(len(mail.outbox), 0) - @override_settings(ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL=" ") - def test_send_notification_email_spaces(self): - """Notification email is sent if ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL is set to only spaces.""" - email_entry = factories.UserEmailEntryFactory.create() - email_entry.send_notification_email() - self.assertEqual(len(mail.outbox), 0) - class AccountTest(TestCase): def test_model_saving(self): From 1bcf5b01482046f92ade81b0a7a0b99213d1daf2 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 11:40:46 -0700 Subject: [PATCH 06/13] Update code for ANVIL_API_SERVICE_ACCOUNT_FILE --- anvil_consortium_manager/anvil_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/anvil_consortium_manager/anvil_api.py b/anvil_consortium_manager/anvil_api.py index ede2ed62..c93bfd70 100644 --- a/anvil_consortium_manager/anvil_api.py +++ b/anvil_consortium_manager/anvil_api.py @@ -13,10 +13,11 @@ import json import logging -from django.conf import settings from google.auth.transport.requests import AuthorizedSession from google.oauth2 import service_account +from . import app_settings + logger = logging.getLogger(__name__) @@ -47,7 +48,7 @@ def __init__(self): This way, all instances should share the same authorized session. """ if AnVILAPIClient.auth_session is None: - credentials = service_account.Credentials.from_service_account_file(settings.ANVIL_API_SERVICE_ACCOUNT_FILE) + credentials = service_account.Credentials.from_service_account_file(app_settings.API_SERVICE_ACCOUNT_FILE) scoped_credentials = credentials.with_scopes( [ "https://www.googleapis.com/auth/userinfo.profile", From 01741da3f46bb3f4c289674b2fb41dc6149edff6 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 13:02:16 -0700 Subject: [PATCH 07/13] Update code for ANVIL_WORKSPACE_ADAPTERS --- anvil_consortium_manager/adapters/workspace.py | 9 +++++---- anvil_consortium_manager/app_settings.py | 8 +++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/anvil_consortium_manager/adapters/workspace.py b/anvil_consortium_manager/adapters/workspace.py index 6dc93b8d..2efd3942 100644 --- a/anvil_consortium_manager/adapters/workspace.py +++ b/anvil_consortium_manager/adapters/workspace.py @@ -2,12 +2,11 @@ from abc import ABC, abstractproperty -from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.forms import ModelForm from django.utils.module_loading import import_string -from .. import models +from .. import app_settings, models class BaseWorkspaceAdapter(ABC): @@ -212,11 +211,13 @@ def get_registered_names(self): def populate_from_settings(self): """Populate the workspace adapter registry from settings. Called by AppConfig ready() method.""" - adapter_modules = settings.ANVIL_WORKSPACE_ADAPTERS + 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) - if not len(adapter_modules): + if not adapter_modules: msg = ( "ANVIL_WORKSPACE_ADAPTERS must specify at least one adapter. Did you mean to use " "the default `anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter`?" diff --git a/anvil_consortium_manager/app_settings.py b/anvil_consortium_manager/app_settings.py index a8cae22e..a0cc2978 100644 --- a/anvil_consortium_manager/app_settings.py +++ b/anvil_consortium_manager/app_settings.py @@ -23,13 +23,19 @@ def API_SERVICE_ACCOUNT_FILE(self): x = self._setting("API_SERVICE_ACCOUNT_FILE") if not x: raise ImproperlyConfigured("ANVIL_API_SERVICE_ACCOUNT_FILE is required in settings.py") + return x @property def WORKSPACE_ADAPTERS(self): """Workspace adapters. Required.""" x = self._setting("WORKSPACE_ADAPTERS") if not x: - raise ImproperlyConfigured("ANVIL_WORKSPACE_ADAPTERS is required in settings.py") + msg = ( + "ANVIL_WORKSPACE_ADAPTERS must specify at least one adapter. Did you mean to use " + "the default `anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter`?" + ) + raise ImproperlyConfigured(msg) + return x @property def ACCOUNT_LINK_EMAIL_SUBJECT(self): From b127a0b30d3e9e7c164180b90495d35f789e6ba7 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 13:02:27 -0700 Subject: [PATCH 08/13] Add tests for app_settings module --- .../tests/test_app_settings.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 anvil_consortium_manager/tests/test_app_settings.py diff --git a/anvil_consortium_manager/tests/test_app_settings.py b/anvil_consortium_manager/tests/test_app_settings.py new file mode 100644 index 00000000..e233b445 --- /dev/null +++ b/anvil_consortium_manager/tests/test_app_settings.py @@ -0,0 +1,76 @@ +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase +from django.test.utils import override_settings + +from .. import app_settings + + +class TestAppSettings(TestCase): + def test_api_service_account_file(self): + # Using test settings. + self.assertEqual(app_settings.API_SERVICE_ACCOUNT_FILE, "foo") + + @override_settings(ANVIL_API_SERVICE_ACCOUNT_FILE=None) + def test_api_service_account_file_none(self): + with self.assertRaisesMessage( + ImproperlyConfigured, "ANVIL_API_SERVICE_ACCOUNT_FILE is required in settings.py" + ): + app_settings.API_SERVICE_ACCOUNT_FILE + + def test_workspace_adapters(self): + # Using test settings. + self.assertEqual( + app_settings.WORKSPACE_ADAPTERS, ["anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter"] + ) + + @override_settings(ANVIL_WORKSPACE_ADAPTERS=None) + def test_workspace_adapters_none(self): + with self.assertRaisesMessage(ImproperlyConfigured, "must specify at least one adapter"): + app_settings.WORKSPACE_ADAPTERS + + @override_settings(ANVIL_WORKSPACE_ADAPTERS=[]) + def test_workspace_adapters_empty_array(self): + with self.assertRaisesMessage(ImproperlyConfigured, "must specify at least one adapter"): + app_settings.WORKSPACE_ADAPTERS + + @override_settings( + ANVIL_WORKSPACE_ADAPTERS=[ + "anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter", + "anvil_consortium_manager.tests.test_app.adapters.TestWorkspaceAdapter", + ] + ) + def test_workspace_adapters_multiple(self): + adapters = app_settings.WORKSPACE_ADAPTERS + self.assertEqual(len(adapters), 2) + self.assertIn("anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter", adapters) + self.assertIn("anvil_consortium_manager.tests.test_app.adapters.TestWorkspaceAdapter", adapters) + + def test_account_link_email_subject(self): + self.assertEqual(app_settings.ACCOUNT_LINK_EMAIL_SUBJECT, "Verify your AnVIL account email") + + @override_settings(ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT="account activation") + def test_account_link_email_subject_custom(self): + self.assertEqual(app_settings.ACCOUNT_LINK_EMAIL_SUBJECT, "account activation") + + def test_account_link_redirect(self): + self.assertEqual(app_settings.ACCOUNT_LINK_REDIRECT, "test_home") + + @override_settings(ANVIL_ACCOUNT_LINK_REDIRECT="test_login") + def test_account_link_redirect_custom(self): + self.assertEqual(app_settings.ACCOUNT_LINK_REDIRECT, "test_login") + + def test_account_verify_notification_email(self): + self.assertEqual(app_settings.ACCOUNT_VERIFY_NOTIFICATION_EMAIL, None) + + @override_settings(ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL="foo@example.com") + def test_account_verify_notification_email_custom(self): + self.assertEqual(app_settings.ACCOUNT_VERIFY_NOTIFICATION_EMAIL, "foo@example.com") + + def test_account_adapter(self): + self.assertEqual( + app_settings.ACCOUNT_ADAPTER, "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" + ) + + @override_settings(ANVIL_ACCOUNT_ADAPTER="anvil_consortium_manager.test_app.adapters.TestAccountAdapter") + def test_account_adapter_custom(self): + self.assertEqual(app_settings.ACCOUNT_ADAPTER, "anvil_consortium_manager.test_app.adapters.TestAccountAdapter") From 6d7aa0bf3200853287c2651097a427e13fa889d3 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 15:14:32 -0700 Subject: [PATCH 09/13] Update docs for new settings structure --- docs/advanced.rst | 10 +++++++--- docs/quickstart.rst | 24 +++++++----------------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index b77a9f17..088ea771 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -3,6 +3,8 @@ Advanced Usage ============== +.. _account_adapter: + The account adapter ------------------- @@ -20,11 +22,13 @@ Optionally, you can override the following methods: - ``get_autocomplete_queryset(self, queryset, q)``: a method that allows the user to provide custom filtering for the autocomplete view. By default, this filters to Accounts whose email contains the case-insensitive search string in ``q``. - ``get_autocomplete_label(self, account)``: a method that allows the user to set the label for an account shown in forms using the autocomplete widget. +.. _workspace_adapter: + The workspace adapter --------------------- The app provides an adapter that you can use to provide extra, customized data about a workspace. -By default, the app uses :class:`~anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter`. +The default workspace adapter provided by the app is :class:`~anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter`. The default ``workspace_data_model`` specified in this adapter has no fields other than those provided by :class:`~anvil_consortium_manager.models.BaseWorkspaceData`. This section describes how to store additional information about a workspace by setting up a custom adapter. @@ -127,8 +131,8 @@ If you would like to display information from the custom workspace data model in {% extends "anvil_consortium_manager/workspace_detail.html" %} {% block workspace_data %}
    -
  • Study name: {{ object.customworkspacedata.study_name }}
  • -
  • Consent: {{ object.customworkspacedata.consent_code }}
  • +
  • Study name: {{ workspace_data_object.study_name }}
  • +
  • Consent: {{ workspace_data_object.consent_code }}
{% endblock workspace_data %} diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b2f10e40..05b7a337 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -44,25 +44,13 @@ Required Settings Alternatively, if you would like to browse the app without making any API, just set this to a random string (e.g., ``"foo"``). -3. Set the default account and workspace adapters in your settings file. +3. Set the ``ANVIL_WORKSPACE_ADAPTERS`` setting in your settings file. .. code-block:: python ANVIL_WORKSPACE_ADAPTERS = ["anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter"] - ANVIL_ACCOUNT_ADAPTER = "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" - See the :ref:`Advanced Usage` section for information about customizing Accounts and Workspaces. - Note that you can have multiple Workspace adapters, but only one Account adapter. - - -4. Add account linking settings to your settings file. - - .. code-block:: python - - # Specify the URL name that AccountLink and AccountLinkVerify redirect to. - ANVIL_ACCOUNT_LINK_REDIRECT = "home" - # Specify the subject for AnVIL account verification emails. - ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = "Verify your AnVIL account email" + For more information about customizing the workspace-related behavior of the app, see the :ref:`workspace_adapter` section. 5. Set up a Site in the sites framework. In your settings file: @@ -72,11 +60,13 @@ Required Settings Optional settings ~~~~~~~~~~~~~~~~~ -If you would like to receive emails when a user links their account, set the ``ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL`` setting in your settings file. - .. code-block:: python +These settings are set to default values automatically, but can be changed by the user in the ``settings.py`` file for further customization. - ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL = "to@example.com" +* ``ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL``: Receive an email when a user links their account (default: None) +* ``ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT``: Subject of the email when a user links their account (default: "AnVIL Account Verification") +* ``ANVIL_ACCOUNT_LINK_REDIRECT_URL``: URL to redirect to after linking an account (default: ``settings.LOGIN_REDIRECT_URL``) +* ``ANVIL_ACCOUNT_ADAPTER``: Adapter to use for Accounts (default: ``"anvil_consortium_manager.adapters.default.DefaultAccountAdapter"``). See the :ref:`account_adapter` section for more information about customizing behavior for accounts. Post-installation From 3be60868051e985cce41ba32d999d484256e523b Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 15:28:54 -0700 Subject: [PATCH 10/13] Remove commented-out code --- anvil_consortium_manager/app_settings.py | 40 ------------------- .../tests/settings/test.py | 4 -- 2 files changed, 44 deletions(-) diff --git a/anvil_consortium_manager/app_settings.py b/anvil_consortium_manager/app_settings.py index a0cc2978..2dfc58b4 100644 --- a/anvil_consortium_manager/app_settings.py +++ b/anvil_consortium_manager/app_settings.py @@ -67,43 +67,3 @@ def ACCOUNT_ADAPTER(self): def __getattr__(name): # See https://peps.python.org/pep-0562/ return getattr(_app_settings, name) - - -# The following settings are used to configure the Anvil Consortium Manager. -# Users should set or override these values in their Django project's settings.py file. -# This file provides defaults for some of the settings. - -# Required settings -# ----------------- - -# # The path to the service account to use for managing access on AnVIL. -# try: -# ANVIL_API_SERVICE_ACCOUNT_FILE = getattr(settings, "ANVIL_API_SERVICE_ACCOUNT_FILE") -# except AttributeError: -# raise ImproperlyConfigured("ANVIL_API_SERVICE_ACCOUNT_FILE is required in settings.py") - -# # Workspace adapters. -# try: -# ANVIL_WORKSPACE_ADAPTERS = getattr(settings, "ANVIL_WORKSPACE_ADAPTERS") -# except AttributeError: -# raise ImproperlyConfigured("ANVIL_WORKSPACE_ADAPTERS is required in settings.py") - -# # Optional settings -# # ----------------- - -# # Subject line for AnVIL account verification emails. -# ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = getattr( -# settings, "ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT", "Verify your AnVIL account email" -# ) - -# # The URL for AccountLinkVerify view redirect -# ANVIL_ACCOUNT_LINK_REDIRECT = getattr(settings, "ANVIL_ACCOUNT_LINK_REDIRECT", settings.LOGIN_REDIRECT_URL) - -# # If desired, specify the email address to send an email to after a user verifies an account. -# # Set to None to disable (default). -# ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL = getattr(settings, "ANVIL_ACCOUNT_VERIFY_NOTIFICATION_EMAIL", None) - -# # Account adapter. -# ANVIL_ACCOUNT_ADAPTER = getattr( -# settings, "ANVIL_ACCOUNT_ADAPTER", "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" -# ) diff --git a/anvil_consortium_manager/tests/settings/test.py b/anvil_consortium_manager/tests/settings/test.py index a3651656..7e9fd426 100644 --- a/anvil_consortium_manager/tests/settings/test.py +++ b/anvil_consortium_manager/tests/settings/test.py @@ -148,10 +148,6 @@ # Path to the service account to use for managing access. # Because the calls are mocked, we don't need to set this. ANVIL_API_SERVICE_ACCOUNT_FILE = "foo" -# ANVIL_ACCOUNT_LINK_REDIRECT = "test_home" -# ANVIL_ACCOUNT_LINK_EMAIL_SUBJECT = "account activation" - ANVIL_WORKSPACE_ADAPTERS = [ "anvil_consortium_manager.adapters.default.DefaultWorkspaceAdapter", ] -# ANVIL_ACCOUNT_ADAPTER = "anvil_consortium_manager.adapters.default.DefaultAccountAdapter" From a6eb219ae9fe64425b714a97eeedea35efab6d23 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 15:58:54 -0700 Subject: [PATCH 11/13] Remove forgotten Django 4.1 if statement Should have done this in the branch dropping support for Django 3.2 but I missed it. --- anvil_consortium_manager/forms.py | 33 +++++++++++-------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/anvil_consortium_manager/forms.py b/anvil_consortium_manager/forms.py index b6ff9128..1e6ee076 100644 --- a/anvil_consortium_manager/forms.py +++ b/anvil_consortium_manager/forms.py @@ -4,9 +4,7 @@ from crispy_forms import layout from crispy_forms.helper import FormHelper from dal import autocomplete, forward -from django import VERSION as DJANGO_VERSION from django import forms -from django.conf import settings from django.core.exceptions import ValidationError from . import models @@ -179,25 +177,18 @@ def clean_billing_project(self): return billing_project def clean(self): - # DJANGO <4.1 on mysql: - # Check for the same case insensitive name in the same billing project. - is_mysql = settings.DATABASES["default"]["ENGINE"] == "django.db.backends.mysql" - if is_mysql and DJANGO_VERSION >= (4, 1): - # This is handled by the model full_clean method with case-insensitive collation. - pass - else: - billing_project = self.cleaned_data.get("billing_project", None) - name = self.cleaned_data.get("name", None) - if ( - billing_project - and name - and models.Workspace.objects.filter( - billing_project=billing_project, - name__iexact=name, - ).exists() - ): - # The workspace already exists - raise a Validation error. - raise ValidationError("Workspace with this Billing Project and Name already exists.") + billing_project = self.cleaned_data.get("billing_project", None) + name = self.cleaned_data.get("name", None) + if ( + billing_project + and name + and models.Workspace.objects.filter( + billing_project=billing_project, + name__iexact=name, + ).exists() + ): + # The workspace already exists - raise a Validation error. + raise ValidationError("Workspace with this Billing Project and Name already exists.") return self.cleaned_data From 3768b0d81d071901b776da17b46b2f1a3ef75ed6 Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 16:15:57 -0700 Subject: [PATCH 12/13] Fix clean method for mysql --- anvil_consortium_manager/forms.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/anvil_consortium_manager/forms.py b/anvil_consortium_manager/forms.py index 1e6ee076..e17d8424 100644 --- a/anvil_consortium_manager/forms.py +++ b/anvil_consortium_manager/forms.py @@ -5,6 +5,7 @@ from crispy_forms.helper import FormHelper from dal import autocomplete, forward from django import forms +from django.conf import settings from django.core.exceptions import ValidationError from . import models @@ -177,18 +178,22 @@ def clean_billing_project(self): return billing_project def clean(self): - billing_project = self.cleaned_data.get("billing_project", None) - name = self.cleaned_data.get("name", None) - if ( - billing_project - and name - and models.Workspace.objects.filter( - billing_project=billing_project, - name__iexact=name, - ).exists() - ): - # The workspace already exists - raise a Validation error. - raise ValidationError("Workspace with this Billing Project and Name already exists.") + # 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 ( + billing_project + and name + and models.Workspace.objects.filter( + billing_project=billing_project, + name__iexact=name, + ).exists() + ): + # The workspace already exists - raise a Validation error. + raise ValidationError("Workspace with this Billing Project and Name already exists.") return self.cleaned_data From c75908c308abf80cec75510ce6083c961297515b Mon Sep 17 00:00:00 2001 From: Adrienne Stilp Date: Wed, 5 Jun 2024 16:33:21 -0700 Subject: [PATCH 13/13] 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 c614a885..4b6b3e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Drop support for Django 3.2. * 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. ## 0.23.0 (2024-05-31) diff --git a/anvil_consortium_manager/__init__.py b/anvil_consortium_manager/__init__.py index 6f03216b..7e7cfa29 100644 --- a/anvil_consortium_manager/__init__.py +++ b/anvil_consortium_manager/__init__.py @@ -1 +1 @@ -__version__ = "0.23.1.dev2" +__version__ = "0.23.1.dev3"