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" 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/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/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", diff --git a/anvil_consortium_manager/app_settings.py b/anvil_consortium_manager/app_settings.py new file mode 100644 index 00000000..2dfc58b4 --- /dev/null +++ b/anvil_consortium_manager/app_settings.py @@ -0,0 +1,69 @@ +# 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") + return x + + @property + def WORKSPACE_ADAPTERS(self): + """Workspace adapters. Required.""" + x = self._setting("WORKSPACE_ADAPTERS") + if not x: + 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): + """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) diff --git a/anvil_consortium_manager/forms.py b/anvil_consortium_manager/forms.py index b6ff9128..e17d8424 100644 --- a/anvil_consortium_manager/forms.py +++ b/anvil_consortium_manager/forms.py @@ -4,7 +4,6 @@ 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 @@ -179,13 +178,10 @@ 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: + 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/models.py b/anvil_consortium_manager/models.py index 19be2c9b..e2ea1a26 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( @@ -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/settings/test.py b/anvil_consortium_manager/tests/settings/test.py index 77a6d8d6..7e9fd426 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,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" 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") diff --git a/anvil_consortium_manager/tests/test_models.py b/anvil_consortium_manager/tests/test_models.py index 517df634..96bb5c85 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) @@ -201,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): diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py index b905c0ac..1d33d104 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 @@ -2140,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)], @@ -2148,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.""" @@ -2163,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)], @@ -2251,7 +2276,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 +2340,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 +2610,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 +2632,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 +2671,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 +2693,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 +2714,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 +2738,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 +2764,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 +2787,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 +2810,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. 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 %}