From d4ee18ee32491c4b41090120b72986de3c7e5222 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 20 Jul 2020 14:08:27 +0200 Subject: [PATCH 1/9] sources/oauth: migrate from discordapp.com to discord.com --- passbook/sources/oauth/forms.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/passbook/sources/oauth/forms.py b/passbook/sources/oauth/forms.py index 509ace23d..5f1e6a65b 100644 --- a/passbook/sources/oauth/forms.py +++ b/passbook/sources/oauth/forms.py @@ -98,9 +98,9 @@ class DiscordOAuthSourceForm(OAuthSourceForm): overrides = { "provider_type": "discord", "request_token_url": "", - "authorization_url": "https://discordapp.com/api/oauth2/authorize", - "access_token_url": "https://discordapp.com/api/oauth2/token", - "profile_url": "https://discordapp.com/api/users/@me", + "authorization_url": "https://discord.com/api/oauth2/authorize", + "access_token_url": "https://discord.com/api/oauth2/token", + "profile_url": "https://discord.com/api/users/@me", } From 74e628ce9c4f3359e34b17eae922b76e47bc1a37 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 20 Jul 2020 14:43:38 +0200 Subject: [PATCH 2/9] ui: allow overriding of verbose_name --- passbook/lib/templatetags/passbook_utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/passbook/lib/templatetags/passbook_utils.py b/passbook/lib/templatetags/passbook_utils.py index aa8cc062f..6acca985b 100644 --- a/passbook/lib/templatetags/passbook_utils.py +++ b/passbook/lib/templatetags/passbook_utils.py @@ -36,7 +36,7 @@ def back(context: Context) -> str: def fieldtype(field): """Return classname""" if isinstance(field.__class__, Model) or issubclass(field.__class__, Model): - return field._meta.verbose_name + return verbose_name(field) return field.__class__.__name__ @@ -84,6 +84,9 @@ def verbose_name(obj) -> str: """Return Object's Verbose Name""" if not obj: return "" + if hasattr(obj, "verbose_name"): + print(obj.verbose_name) + return obj.verbose_name return obj._meta.verbose_name @@ -92,7 +95,7 @@ def form_verbose_name(obj) -> str: """Return ModelForm's Object's Verbose Name""" if not obj: return "" - return obj._meta.model._meta.verbose_name + return verbose_name(obj._meta.model) @register.filter From ac2dd3611fd70c880c2ce356651aee54c11014cf Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 20 Jul 2020 15:11:27 +0200 Subject: [PATCH 3/9] sources/*: remove path-based import from all sources --- e2e/test_provider_oauth.py | 12 ++++----- e2e/test_provider_oidc.py | 12 ++++----- passbook/core/models.py | 14 ++++++++--- passbook/sources/ldap/models.py | 8 ++++-- passbook/sources/oauth/models.py | 42 +++++++++++++++++++++++++------- passbook/sources/saml/models.py | 8 +++++- 6 files changed, 68 insertions(+), 28 deletions(-) diff --git a/e2e/test_provider_oauth.py b/e2e/test_provider_oauth.py index 6b40bbfc7..8733a15e7 100644 --- a/e2e/test_provider_oauth.py +++ b/e2e/test_provider_oauth.py @@ -103,9 +103,9 @@ class TestProviderOAuth(SeleniumTestCase): USER().username, ) self.assertEqual( - self.driver.find_element( - By.CSS_SELECTOR, "input[name=name]" - ).get_attribute("value"), + self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( + "value" + ), USER().username, ) self.assertEqual( @@ -172,9 +172,9 @@ class TestProviderOAuth(SeleniumTestCase): USER().username, ) self.assertEqual( - self.driver.find_element( - By.CSS_SELECTOR, "input[name=name]" - ).get_attribute("value"), + self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( + "value" + ), USER().username, ) self.assertEqual( diff --git a/e2e/test_provider_oidc.py b/e2e/test_provider_oidc.py index c9ae3524c..779a048bf 100644 --- a/e2e/test_provider_oidc.py +++ b/e2e/test_provider_oidc.py @@ -153,9 +153,9 @@ class TestProviderOIDC(SeleniumTestCase): USER().name, ) self.assertEqual( - self.driver.find_element( - By.CSS_SELECTOR, "input[name=name]" - ).get_attribute("value"), + self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( + "value" + ), USER().name, ) self.assertEqual( @@ -232,9 +232,9 @@ class TestProviderOIDC(SeleniumTestCase): USER().name, ) self.assertEqual( - self.driver.find_element( - By.CSS_SELECTOR, "input[name=name]" - ).get_attribute("value"), + self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute( + "value" + ), USER().name, ) self.assertEqual( diff --git a/passbook/core/models.py b/passbook/core/models.py index 3febb463a..ad91b616c 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -1,12 +1,13 @@ """passbook core models""" from datetime import timedelta -from typing import Any, Optional +from typing import Any, Optional, Type from uuid import uuid4 from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import JSONField from django.db import models from django.db.models import Q, QuerySet +from django.forms import ModelForm from django.http import HttpRequest from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ @@ -162,10 +163,12 @@ class Source(PolicyBindingModel): related_name="source_enrollment", ) - form = "" # ModelForm-based class ued to create/edit instance - objects = InheritanceManager() + def form(self) -> Type[ModelForm]: + """Return Form class used to edit this object""" + raise NotImplementedError + @property def ui_login_button(self) -> Optional[UILoginButton]: """If source uses a http-based flow, return UI Information about the login @@ -261,9 +264,12 @@ class PropertyMapping(models.Model): name = models.TextField() expression = models.TextField() - form = "" objects = InheritanceManager() + def form(self) -> Type[ModelForm]: + """Return Form class used to edit this object""" + raise NotImplementedError + def evaluate( self, user: Optional[User], request: Optional[HttpRequest], **kwargs ) -> Any: diff --git a/passbook/sources/ldap/models.py b/passbook/sources/ldap/models.py index 0de975870..b4581d11d 100644 --- a/passbook/sources/ldap/models.py +++ b/passbook/sources/ldap/models.py @@ -1,8 +1,9 @@ """passbook LDAP Models""" -from typing import Optional +from typing import Optional, Type from django.core.validators import URLValidator from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from ldap3 import Connection, Server @@ -53,7 +54,10 @@ class LDAPSource(Source): Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT ) - form = "passbook.sources.ldap.forms.LDAPSourceForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.ldap.forms import LDAPSourceForm + + return LDAPSourceForm _connection: Optional[Connection] = None diff --git a/passbook/sources/oauth/models.py b/passbook/sources/oauth/models.py index 95262f32f..ff97d84f2 100644 --- a/passbook/sources/oauth/models.py +++ b/passbook/sources/oauth/models.py @@ -1,7 +1,8 @@ """OAuth Client models""" -from typing import Optional +from typing import Optional, Type from django.db import models +from django.forms import ModelForm from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ @@ -40,7 +41,10 @@ class OAuthSource(Source): consumer_key = models.TextField() consumer_secret = models.TextField() - form = "passbook.sources.oauth.forms.OAuthSourceForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.oauth.forms import OAuthSourceForm + + return OAuthSourceForm @property def ui_login_button(self) -> UILoginButton: @@ -80,7 +84,10 @@ class OAuthSource(Source): class GitHubOAuthSource(OAuthSource): """Social Login using GitHub.com or a GitHub-Enterprise Instance.""" - form = "passbook.sources.oauth.forms.GitHubOAuthSourceForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.oauth.forms import GitHubOAuthSourceForm + + return GitHubOAuthSourceForm class Meta: @@ -92,7 +99,9 @@ class GitHubOAuthSource(OAuthSource): # class TwitterOAuthSource(OAuthSource): # """Social Login using Twitter.com""" -# form = "passbook.sources.oauth.forms.TwitterOAuthSourceForm" +# def form(self) -> Type[ModelForm]: +# from passbook.sources.oauth.forms import TwitterOAuthSourceForm +# return TwitterOAuthSourceForm # class Meta: @@ -104,7 +113,10 @@ class GitHubOAuthSource(OAuthSource): class FacebookOAuthSource(OAuthSource): """Social Login using Facebook.com.""" - form = "passbook.sources.oauth.forms.FacebookOAuthSourceForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.oauth.forms import FacebookOAuthSourceForm + + return FacebookOAuthSourceForm class Meta: @@ -116,7 +128,10 @@ class FacebookOAuthSource(OAuthSource): class DiscordOAuthSource(OAuthSource): """Social Login using Discord.""" - form = "passbook.sources.oauth.forms.DiscordOAuthSourceForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.oauth.forms import DiscordOAuthSourceForm + + return DiscordOAuthSourceForm class Meta: @@ -128,7 +143,10 @@ class DiscordOAuthSource(OAuthSource): class GoogleOAuthSource(OAuthSource): """Social Login using Google or Gsuite.""" - form = "passbook.sources.oauth.forms.GoogleOAuthSourceForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.oauth.forms import GoogleOAuthSourceForm + + return GoogleOAuthSourceForm class Meta: @@ -140,7 +158,10 @@ class GoogleOAuthSource(OAuthSource): class AzureADOAuthSource(OAuthSource): """Social Login using Azure AD.""" - form = "passbook.sources.oauth.forms.AzureADOAuthSourceForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.oauth.forms import AzureADOAuthSourceForm + + return AzureADOAuthSourceForm class Meta: @@ -152,7 +173,10 @@ class AzureADOAuthSource(OAuthSource): class OpenIDOAuthSource(OAuthSource): """Login using a Generic OpenID-Connect compliant provider.""" - form = "passbook.sources.oauth.forms.OAuthSourceForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.oauth.forms import OAuthSourceForm + + return OAuthSourceForm class Meta: diff --git a/passbook/sources/saml/models.py b/passbook/sources/saml/models.py index 33e1b3da9..9a76a83d8 100644 --- a/passbook/sources/saml/models.py +++ b/passbook/sources/saml/models.py @@ -1,5 +1,8 @@ """saml sp models""" +from typing import Type + from django.db import models +from django.forms import ModelForm from django.http import HttpRequest from django.shortcuts import reverse from django.urls import reverse_lazy @@ -93,7 +96,10 @@ class SAMLSource(Source): on_delete=models.PROTECT, ) - form = "passbook.sources.saml.forms.SAMLSourceForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.saml.forms import SAMLSourceForm + + return SAMLSourceForm def get_issuer(self, request: HttpRequest) -> str: """Get Source's Issuer, falling back to our Metadata URL if none is set""" From 6aefd072c881870948f746992803a181eb65d1e1 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 20 Jul 2020 15:58:48 +0200 Subject: [PATCH 4/9] policies/*: remove path-based import from all policies --- passbook/policies/dummy/models.py | 7 ++++++- passbook/policies/expiry/models.py | 7 ++++++- passbook/policies/expression/models.py | 8 +++++++- passbook/policies/group_membership/models.py | 8 +++++++- passbook/policies/hibp/models.py | 7 ++++++- passbook/policies/models.py | 6 ++++++ passbook/policies/password/models.py | 7 ++++++- passbook/policies/reputation/models.py | 8 +++++++- 8 files changed, 51 insertions(+), 7 deletions(-) diff --git a/passbook/policies/dummy/models.py b/passbook/policies/dummy/models.py index a8172157a..497f7a0fb 100644 --- a/passbook/policies/dummy/models.py +++ b/passbook/policies/dummy/models.py @@ -1,8 +1,10 @@ """Dummy policy""" from random import SystemRandom from time import sleep +from typing import Type from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from structlog import get_logger @@ -22,7 +24,10 @@ class DummyPolicy(Policy): wait_min = models.IntegerField(default=5) wait_max = models.IntegerField(default=30) - form = "passbook.policies.dummy.forms.DummyPolicyForm" + def form(self) -> Type[ModelForm]: + from passbook.policies.dummy.forms import DummyPolicyForm + + return DummyPolicyForm def passes(self, request: PolicyRequest) -> PolicyResult: """Wait random time then return result""" diff --git a/passbook/policies/expiry/models.py b/passbook/policies/expiry/models.py index dfba3b4b9..f476c4dc0 100644 --- a/passbook/policies/expiry/models.py +++ b/passbook/policies/expiry/models.py @@ -1,7 +1,9 @@ """passbook password_expiry_policy Models""" from datetime import timedelta +from typing import Type from django.db import models +from django.forms import ModelForm from django.utils.timezone import now from django.utils.translation import gettext as _ from structlog import get_logger @@ -19,7 +21,10 @@ class PasswordExpiryPolicy(Policy): deny_only = models.BooleanField(default=False) days = models.IntegerField() - form = "passbook.policies.expiry.forms.PasswordExpiryPolicyForm" + def form(self) -> Type[ModelForm]: + from passbook.policies.expiry.forms import PasswordExpiryPolicyForm + + return PasswordExpiryPolicyForm def passes(self, request: PolicyRequest) -> PolicyResult: """If password change date is more than x days in the past, call set_unusable_password diff --git a/passbook/policies/expression/models.py b/passbook/policies/expression/models.py index 67137d0b6..31c3c398e 100644 --- a/passbook/policies/expression/models.py +++ b/passbook/policies/expression/models.py @@ -1,5 +1,8 @@ """passbook expression Policy Models""" +from typing import Type + from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext as _ from passbook.policies.expression.evaluator import PolicyEvaluator @@ -12,7 +15,10 @@ class ExpressionPolicy(Policy): expression = models.TextField() - form = "passbook.policies.expression.forms.ExpressionPolicyForm" + def form(self) -> Type[ModelForm]: + from passbook.policies.expression.forms import ExpressionPolicyForm + + return ExpressionPolicyForm def passes(self, request: PolicyRequest) -> PolicyResult: """Evaluate and render expression. Returns PolicyResult(false) on error.""" diff --git a/passbook/policies/group_membership/models.py b/passbook/policies/group_membership/models.py index 730b49a9e..7f87a9a00 100644 --- a/passbook/policies/group_membership/models.py +++ b/passbook/policies/group_membership/models.py @@ -1,5 +1,8 @@ """user field matcher models""" +from typing import Type + from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext as _ from passbook.core.models import Group @@ -12,7 +15,10 @@ class GroupMembershipPolicy(Policy): group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL) - form = "passbook.policies.group_membership.forms.GroupMembershipPolicyForm" + def form(self) -> Type[ModelForm]: + from passbook.policies.group_membership.forms import GroupMembershipPolicyForm + + return GroupMembershipPolicyForm def passes(self, request: PolicyRequest) -> PolicyResult: return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists()) diff --git a/passbook/policies/hibp/models.py b/passbook/policies/hibp/models.py index 90407321d..f9a27fd19 100644 --- a/passbook/policies/hibp/models.py +++ b/passbook/policies/hibp/models.py @@ -1,7 +1,9 @@ """passbook HIBP Models""" from hashlib import sha1 +from typing import Type from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext as _ from requests import get from structlog import get_logger @@ -25,7 +27,10 @@ class HaveIBeenPwendPolicy(Policy): allowed_count = models.IntegerField(default=0) - form = "passbook.policies.hibp.forms.HaveIBeenPwnedPolicyForm" + def form(self) -> Type[ModelForm]: + from passbook.policies.hibp.forms import HaveIBeenPwnedPolicyForm + + return HaveIBeenPwnedPolicyForm def passes(self, request: PolicyRequest) -> PolicyResult: """Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5 diff --git a/passbook/policies/models.py b/passbook/policies/models.py index 826938f51..06c096ec7 100644 --- a/passbook/policies/models.py +++ b/passbook/policies/models.py @@ -1,7 +1,9 @@ """Policy base models""" +from typing import Type from uuid import uuid4 from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ from model_utils.managers import InheritanceManager @@ -73,6 +75,10 @@ class Policy(CreatedUpdatedModel): objects = InheritanceAutoManager() + def form(self) -> Type[ModelForm]: + """Return Form class used to edit this object""" + raise NotImplementedError + def __str__(self): return f"Policy {self.name}" diff --git a/passbook/policies/password/models.py b/passbook/policies/password/models.py index f0250a2f4..bdd40bd92 100644 --- a/passbook/policies/password/models.py +++ b/passbook/policies/password/models.py @@ -1,7 +1,9 @@ """user field matcher models""" import re +from typing import Type from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext as _ from structlog import get_logger @@ -28,7 +30,10 @@ class PasswordPolicy(Policy): symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ") error_message = models.TextField() - form = "passbook.policies.password.forms.PasswordPolicyForm" + def form(self) -> Type[ModelForm]: + from passbook.policies.password.forms import PasswordPolicyForm + + return PasswordPolicyForm def passes(self, request: PolicyRequest) -> PolicyResult: if self.password_field not in request.context: diff --git a/passbook/policies/reputation/models.py b/passbook/policies/reputation/models.py index aa4326129..9ed0366a4 100644 --- a/passbook/policies/reputation/models.py +++ b/passbook/policies/reputation/models.py @@ -1,6 +1,9 @@ """passbook reputation request policy""" +from typing import Type + from django.core.cache import cache from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext as _ from passbook.core.models import User @@ -19,7 +22,10 @@ class ReputationPolicy(Policy): check_username = models.BooleanField(default=True) threshold = models.IntegerField(default=-5) - form = "passbook.policies.reputation.forms.ReputationPolicyForm" + def form(self) -> Type[ModelForm]: + from passbook.policies.reputation.forms import ReputationPolicyForm + + return ReputationPolicyForm def passes(self, request: PolicyRequest) -> PolicyResult: remote_ip = get_client_ip(request.http_request) or "255.255.255.255" From 6fa825e372942dbc9f3733ffdac888d44f862ec5 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 20 Jul 2020 16:03:55 +0200 Subject: [PATCH 5/9] providers/*: remove path-based import from all providers --- passbook/core/models.py | 4 ++++ passbook/providers/app_gw/models.py | 8 ++++++-- passbook/providers/oauth/models.py | 8 ++++++-- passbook/providers/oidc/models.py | 8 ++++++-- passbook/providers/saml/models.py | 8 ++++++-- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/passbook/core/models.py b/passbook/core/models.py index ad91b616c..fb29c1c64 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -93,6 +93,10 @@ class Provider(models.Model): objects = InheritanceManager() + def form(self) -> Type[ModelForm]: + """Return Form class used to edit this object""" + raise NotImplementedError + # This class defines no field for easier inheritance def __str__(self): if hasattr(self, "name"): diff --git a/passbook/providers/app_gw/models.py b/passbook/providers/app_gw/models.py index efacbafb9..88771ae7c 100644 --- a/passbook/providers/app_gw/models.py +++ b/passbook/providers/app_gw/models.py @@ -1,9 +1,10 @@ """passbook app_gw models""" import string from random import SystemRandom -from typing import Optional +from typing import Optional, Type from django.db import models +from django.forms import ModelForm from django.http import HttpRequest from django.utils.translation import gettext as _ from oidc_provider.models import Client @@ -23,7 +24,10 @@ class ApplicationGatewayProvider(Provider): client = models.ForeignKey(Client, on_delete=models.CASCADE) - form = "passbook.providers.app_gw.forms.ApplicationGatewayProviderForm" + def form(self) -> Type[ModelForm]: + from passbook.providers.app_gw.forms import ApplicationGatewayProviderForm + + return ApplicationGatewayProviderForm def html_setup_urls(self, request: HttpRequest) -> Optional[str]: """return template and context modal with URLs for authorize, token, openid-config, etc""" diff --git a/passbook/providers/oauth/models.py b/passbook/providers/oauth/models.py index fe0167d98..f1a9ad522 100644 --- a/passbook/providers/oauth/models.py +++ b/passbook/providers/oauth/models.py @@ -1,7 +1,8 @@ """Oauth2 provider product extension""" -from typing import Optional +from typing import Optional, Type +from django.forms import ModelForm from django.http import HttpRequest from django.shortcuts import reverse from django.utils.translation import gettext as _ @@ -16,7 +17,10 @@ class OAuth2Provider(Provider, AbstractApplication): This Provider also supports the GitHub-pretend mode for Applications that don't support generic OAuth.""" - form = "passbook.providers.oauth.forms.OAuth2ProviderForm" + def form(self) -> Type[ModelForm]: + from passbook.providers.oauth.forms import OAuth2ProviderForm + + return OAuth2ProviderForm def __str__(self): return self.name diff --git a/passbook/providers/oidc/models.py b/passbook/providers/oidc/models.py index 5774d7201..6f74ccb53 100644 --- a/passbook/providers/oidc/models.py +++ b/passbook/providers/oidc/models.py @@ -1,7 +1,8 @@ """oidc models""" -from typing import Optional +from typing import Optional, Type from django.db import models +from django.forms import ModelForm from django.http import HttpRequest from django.shortcuts import reverse from django.utils.translation import gettext as _ @@ -20,7 +21,10 @@ class OpenIDProvider(Provider): oidc_client = models.OneToOneField(Client, on_delete=models.CASCADE) - form = "passbook.providers.oidc.forms.OIDCProviderForm" + def form(self) -> Type[ModelForm]: + from passbook.providers.oidc.forms import OIDCProviderForm + + return OIDCProviderForm @property def name(self): diff --git a/passbook/providers/saml/models.py b/passbook/providers/saml/models.py index 56b6a8c74..2e7bd68f4 100644 --- a/passbook/providers/saml/models.py +++ b/passbook/providers/saml/models.py @@ -1,7 +1,8 @@ """passbook saml_idp Models""" -from typing import Optional +from typing import Optional, Type from django.db import models +from django.forms import ModelForm from django.http import HttpRequest from django.shortcuts import reverse from django.utils.translation import ugettext_lazy as _ @@ -101,7 +102,10 @@ class SAMLProvider(Provider): ), ) - form = "passbook.providers.saml.forms.SAMLProviderForm" + def form(self) -> Type[ModelForm]: + from passbook.providers.saml.forms import SAMLProviderForm + + return SAMLProviderForm def __str__(self): return self.name From a3d92ebc0a9744efbe3f3940d83e357fad0c63d5 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 20 Jul 2020 16:23:30 +0200 Subject: [PATCH 6/9] stages/*: remove path-based import from all stages --- passbook/stages/captcha/models.py | 15 +++++++++++++-- passbook/stages/captcha/stage.py | 2 +- passbook/stages/consent/models.py | 15 +++++++++++++-- passbook/stages/consent/stage.py | 2 +- passbook/stages/dummy/models.py | 15 +++++++++++++-- passbook/stages/dummy/stage.py | 2 +- passbook/stages/email/models.py | 15 +++++++++++++-- passbook/stages/identification/models.py | 15 +++++++++++++-- passbook/stages/invitation/models.py | 14 ++++++++++++-- passbook/stages/otp_static/models.py | 15 ++++++++++++--- passbook/stages/otp_time/models.py | 15 ++++++++++++--- passbook/stages/otp_validate/models.py | 15 +++++++++++++-- passbook/stages/password/models.py | 15 ++++++++++++--- passbook/stages/password/stage.py | 2 +- passbook/stages/prompt/models.py | 14 ++++++++++++-- passbook/stages/user_delete/models.py | 15 +++++++++++++-- passbook/stages/user_login/models.py | 15 +++++++++++++-- passbook/stages/user_logout/models.py | 15 +++++++++++++-- passbook/stages/user_write/models.py | 15 +++++++++++++-- 19 files changed, 194 insertions(+), 37 deletions(-) diff --git a/passbook/stages/captcha/models.py b/passbook/stages/captcha/models.py index db52e4722..584a5ca41 100644 --- a/passbook/stages/captcha/models.py +++ b/passbook/stages/captcha/models.py @@ -1,6 +1,10 @@ """passbook captcha stage""" +from typing import Type + from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.flows.models import Stage @@ -19,8 +23,15 @@ class CaptchaStage(Stage): ) ) - type = "passbook.stages.captcha.stage.CaptchaStage" - form = "passbook.stages.captcha.forms.CaptchaStageForm" + def type(self) -> Type[View]: + from passbook.stages.captcha.stage import CaptchaStageView + + return CaptchaStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.captcha.forms import CaptchaStageForm + + return CaptchaStageForm def __str__(self): return f"Captcha Stage {self.name}" diff --git a/passbook/stages/captcha/stage.py b/passbook/stages/captcha/stage.py index 69e789ac3..f758506e7 100644 --- a/passbook/stages/captcha/stage.py +++ b/passbook/stages/captcha/stage.py @@ -6,7 +6,7 @@ from passbook.flows.stage import StageView from passbook.stages.captcha.forms import CaptchaForm -class CaptchaStage(FormView, StageView): +class CaptchaStageView(FormView, StageView): """Simple captcha checker, logic is handeled in django-captcha module""" form_class = CaptchaForm diff --git a/passbook/stages/consent/models.py b/passbook/stages/consent/models.py index 3132474c5..5c3679391 100644 --- a/passbook/stages/consent/models.py +++ b/passbook/stages/consent/models.py @@ -1,5 +1,9 @@ """passbook consent stage""" +from typing import Type + +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.flows.models import Stage @@ -7,8 +11,15 @@ from passbook.flows.models import Stage class ConsentStage(Stage): """Prompt the user for confirmation.""" - type = "passbook.stages.consent.stage.ConsentStage" - form = "passbook.stages.consent.forms.ConsentStageForm" + def type(self) -> Type[View]: + from passbook.stages.consent.stage import ConsentStageView + + return ConsentStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.consent.forms import ConsentStageForm + + return ConsentStageForm def __str__(self): return f"Consent Stage {self.name}" diff --git a/passbook/stages/consent/stage.py b/passbook/stages/consent/stage.py index 3a1fa14a2..9ad58fde2 100644 --- a/passbook/stages/consent/stage.py +++ b/passbook/stages/consent/stage.py @@ -9,7 +9,7 @@ from passbook.stages.consent.forms import ConsentForm PLAN_CONTEXT_CONSENT_TEMPLATE = "consent_template" -class ConsentStage(FormView, StageView): +class ConsentStageView(FormView, StageView): """Simple consent checker.""" form_class = ConsentForm diff --git a/passbook/stages/dummy/models.py b/passbook/stages/dummy/models.py index 3dfb1d891..14d77ba6a 100644 --- a/passbook/stages/dummy/models.py +++ b/passbook/stages/dummy/models.py @@ -1,5 +1,9 @@ """dummy stage models""" +from typing import Type + +from django.forms import ModelForm from django.utils.translation import gettext as _ +from django.views import View from passbook.flows.models import Stage @@ -9,8 +13,15 @@ class DummyStage(Stage): __debug_only__ = True - type = "passbook.stages.dummy.stage.DummyStage" - form = "passbook.stages.dummy.forms.DummyStageForm" + def type(self) -> Type[View]: + from passbook.stages.dummy.stage import DummyStageView + + return DummyStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.dummy.forms import DummyStageForm + + return DummyStageForm def __str__(self): return f"Dummy Stage {self.name}" diff --git a/passbook/stages/dummy/stage.py b/passbook/stages/dummy/stage.py index 07f3dacff..bb0620cef 100644 --- a/passbook/stages/dummy/stage.py +++ b/passbook/stages/dummy/stage.py @@ -6,7 +6,7 @@ from django.http import HttpRequest from passbook.flows.stage import StageView -class DummyStage(StageView): +class DummyStageView(StageView): """Dummy stage for testing with multiple stages""" def post(self, request: HttpRequest): diff --git a/passbook/stages/email/models.py b/passbook/stages/email/models.py index 6508704b3..3c575f692 100644 --- a/passbook/stages/email/models.py +++ b/passbook/stages/email/models.py @@ -1,8 +1,12 @@ """email stage models""" +from typing import Type + from django.core.mail import get_connection from django.core.mail.backends.base import BaseEmailBackend from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext as _ +from django.views import View from passbook.flows.models import Stage @@ -40,8 +44,15 @@ class EmailStage(Stage): choices=EmailTemplates.choices, default=EmailTemplates.PASSWORD_RESET ) - type = "passbook.stages.email.stage.EmailStageView" - form = "passbook.stages.email.forms.EmailStageForm" + def type(self) -> Type[View]: + from passbook.stages.email.stage import EmailStageView + + return EmailStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.email.forms import EmailStageForm + + return EmailStageForm @property def backend(self) -> BaseEmailBackend: diff --git a/passbook/stages/identification/models.py b/passbook/stages/identification/models.py index bc5c4cecc..86963d931 100644 --- a/passbook/stages/identification/models.py +++ b/passbook/stages/identification/models.py @@ -1,7 +1,11 @@ """identification stage models""" +from typing import Type + from django.contrib.postgres.fields import ArrayField from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.flows.models import Flow, Stage @@ -52,8 +56,15 @@ class IdentificationStage(Stage): ), ) - type = "passbook.stages.identification.stage.IdentificationStageView" - form = "passbook.stages.identification.forms.IdentificationStageForm" + def type(self) -> Type[View]: + from passbook.stages.identification.stage import IdentificationStageView + + return IdentificationStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.identification.forms import IdentificationStageForm + + return IdentificationStageForm def __str__(self): return f"Identification Stage {self.name}" diff --git a/passbook/stages/invitation/models.py b/passbook/stages/invitation/models.py index 5b9024aec..db7e47011 100644 --- a/passbook/stages/invitation/models.py +++ b/passbook/stages/invitation/models.py @@ -1,9 +1,12 @@ """invitation stage models""" +from typing import Type from uuid import uuid4 from django.contrib.postgres.fields import JSONField from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.core.models import User from passbook.flows.models import Stage @@ -24,8 +27,15 @@ class InvitationStage(Stage): ), ) - type = "passbook.stages.invitation.stage.InvitationStageView" - form = "passbook.stages.invitation.forms.InvitationStageForm" + def type(self) -> Type[View]: + from passbook.stages.invitation.stage import InvitationStageView + + return InvitationStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.invitation.forms import InvitationStageForm + + return InvitationStageForm def __str__(self): return f"Invitation Stage {self.name}" diff --git a/passbook/stages/otp_static/models.py b/passbook/stages/otp_static/models.py index 0d6b9a7a0..7005fed8c 100644 --- a/passbook/stages/otp_static/models.py +++ b/passbook/stages/otp_static/models.py @@ -1,9 +1,11 @@ """OTP Static models""" -from typing import Optional +from typing import Optional, Type from django.db import models +from django.forms import ModelForm from django.shortcuts import reverse from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.core.types import UIUserSettings from passbook.flows.models import Stage @@ -14,8 +16,15 @@ class OTPStaticStage(Stage): token_count = models.IntegerField(default=6) - type = "passbook.stages.otp_static.stage.OTPStaticStageView" - form = "passbook.stages.otp_static.forms.OTPStaticStageForm" + def type(self) -> Type[View]: + from passbook.stages.otp_static.stage import OTPStaticStageView + + return OTPStaticStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.otp_static.forms import OTPStaticStageForm + + return OTPStaticStageForm @property def ui_user_settings(self) -> Optional[UIUserSettings]: diff --git a/passbook/stages/otp_time/models.py b/passbook/stages/otp_time/models.py index 73b6840e5..7b9460cc5 100644 --- a/passbook/stages/otp_time/models.py +++ b/passbook/stages/otp_time/models.py @@ -1,9 +1,11 @@ """OTP Time-based models""" -from typing import Optional +from typing import Optional, Type from django.db import models +from django.forms import ModelForm from django.shortcuts import reverse from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.core.types import UIUserSettings from passbook.flows.models import Stage @@ -21,8 +23,15 @@ class OTPTimeStage(Stage): digits = models.IntegerField(choices=TOTPDigits.choices) - type = "passbook.stages.otp_time.stage.OTPTimeStageView" - form = "passbook.stages.otp_time.forms.OTPTimeStageForm" + def type(self) -> Type[View]: + from passbook.stages.otp_time.stage import OTPTimeStageView + + return OTPTimeStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.otp_time.forms import OTPTimeStageForm + + return OTPTimeStageForm @property def ui_user_settings(self) -> Optional[UIUserSettings]: diff --git a/passbook/stages/otp_validate/models.py b/passbook/stages/otp_validate/models.py index 4015aa365..f89735025 100644 --- a/passbook/stages/otp_validate/models.py +++ b/passbook/stages/otp_validate/models.py @@ -1,6 +1,10 @@ """OTP Validation Stage""" +from typing import Type + from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.flows.models import NotConfiguredAction, Stage @@ -12,8 +16,15 @@ class OTPValidateStage(Stage): choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP ) - type = "passbook.stages.otp_validate.stage.OTPValidateStageView" - form = "passbook.stages.otp_validate.forms.OTPValidateStageForm" + def type(self) -> Type[View]: + from passbook.stages.otp_validate.stage import OTPValidateStageView + + return OTPValidateStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.otp_validate.forms import OTPValidateStageForm + + return OTPValidateStageForm def __str__(self) -> str: return f"OTP Validation Stage {self.name}" diff --git a/passbook/stages/password/models.py b/passbook/stages/password/models.py index 24275916f..c9d7fce3c 100644 --- a/passbook/stages/password/models.py +++ b/passbook/stages/password/models.py @@ -1,11 +1,13 @@ """password stage models""" -from typing import Optional +from typing import Optional, Type from django.contrib.postgres.fields import ArrayField from django.db import models +from django.forms import ModelForm from django.shortcuts import reverse from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.core.types import UIUserSettings from passbook.flows.models import Flow, Stage @@ -33,8 +35,15 @@ class PasswordStage(Stage): ), ) - type = "passbook.stages.password.stage.PasswordStage" - form = "passbook.stages.password.forms.PasswordStageForm" + def type(self) -> Type[View]: + from passbook.stages.password.stage import PasswordStageView + + return PasswordStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.password.forms import PasswordStageForm + + return PasswordStageForm @property def ui_user_settings(self) -> Optional[UIUserSettings]: diff --git a/passbook/stages/password/stage.py b/passbook/stages/password/stage.py index 8dd708f25..9a48fb3c0 100644 --- a/passbook/stages/password/stage.py +++ b/passbook/stages/password/stage.py @@ -46,7 +46,7 @@ def authenticate( ) -class PasswordStage(FormView, StageView): +class PasswordStageView(FormView, StageView): """Authentication stage which authenticates against django's AuthBackend""" form_class = PasswordForm diff --git a/passbook/stages/prompt/models.py b/passbook/stages/prompt/models.py index 9c44c0ed5..d641f7405 100644 --- a/passbook/stages/prompt/models.py +++ b/passbook/stages/prompt/models.py @@ -1,9 +1,12 @@ """prompt models""" +from typing import Type from uuid import uuid4 from django import forms from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.flows.models import Stage from passbook.policies.models import PolicyBindingModel @@ -117,8 +120,15 @@ class PromptStage(PolicyBindingModel, Stage): fields = models.ManyToManyField(Prompt) - type = "passbook.stages.prompt.stage.PromptStageView" - form = "passbook.stages.prompt.forms.PromptStageForm" + def type(self) -> Type[View]: + from passbook.stages.prompt.stage import PromptStageView + + return PromptStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.prompt.forms import PromptStageForm + + return PromptStageForm def __str__(self): return f"Prompt Stage {self.name}" diff --git a/passbook/stages/user_delete/models.py b/passbook/stages/user_delete/models.py index 1e9483ad3..667673c72 100644 --- a/passbook/stages/user_delete/models.py +++ b/passbook/stages/user_delete/models.py @@ -1,5 +1,9 @@ """delete stage models""" +from typing import Type + +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.flows.models import Stage @@ -8,8 +12,15 @@ class UserDeleteStage(Stage): """Deletes the currently pending user without confirmation. Use with caution.""" - type = "passbook.stages.user_delete.stage.UserDeleteStageView" - form = "passbook.stages.user_delete.forms.UserDeleteStageForm" + def type(self) -> Type[View]: + from passbook.stages.user_delete.stage import UserDeleteStageView + + return UserDeleteStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.user_delete.forms import UserDeleteStageForm + + return UserDeleteStageForm def __str__(self): return f"User Delete Stage {self.name}" diff --git a/passbook/stages/user_login/models.py b/passbook/stages/user_login/models.py index 08497983f..96c86661e 100644 --- a/passbook/stages/user_login/models.py +++ b/passbook/stages/user_login/models.py @@ -1,6 +1,10 @@ """login stage models""" +from typing import Type + from django.db import models +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.flows.models import Stage @@ -16,8 +20,15 @@ class UserLoginStage(Stage): ), ) - type = "passbook.stages.user_login.stage.UserLoginStageView" - form = "passbook.stages.user_login.forms.UserLoginStageForm" + def type(self) -> Type[View]: + from passbook.stages.user_login.stage import UserLoginStageView + + return UserLoginStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.user_login.forms import UserLoginStageForm + + return UserLoginStageForm def __str__(self): return f"User Login Stage {self.name}" diff --git a/passbook/stages/user_logout/models.py b/passbook/stages/user_logout/models.py index 51833bfe1..d85bf2252 100644 --- a/passbook/stages/user_logout/models.py +++ b/passbook/stages/user_logout/models.py @@ -1,5 +1,9 @@ """logout stage models""" +from typing import Type + +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.flows.models import Stage @@ -7,8 +11,15 @@ from passbook.flows.models import Stage class UserLogoutStage(Stage): """Resets the users current session.""" - type = "passbook.stages.user_logout.stage.UserLogoutStageView" - form = "passbook.stages.user_logout.forms.UserLogoutStageForm" + def type(self) -> Type[View]: + from passbook.stages.user_logout.stage import UserLogoutStageView + + return UserLogoutStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.user_logout.forms import UserLogoutStageForm + + return UserLogoutStageForm def __str__(self): return f"User Logout Stage {self.name}" diff --git a/passbook/stages/user_write/models.py b/passbook/stages/user_write/models.py index 140b5623d..bc7635b6d 100644 --- a/passbook/stages/user_write/models.py +++ b/passbook/stages/user_write/models.py @@ -1,5 +1,9 @@ """write stage models""" +from typing import Type + +from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ +from django.views import View from passbook.flows.models import Stage @@ -8,8 +12,15 @@ class UserWriteStage(Stage): """Writes currently pending data into the pending user, or if no user exists, creates a new user with the data.""" - type = "passbook.stages.user_write.stage.UserWriteStageView" - form = "passbook.stages.user_write.forms.UserWriteStageForm" + def type(self) -> Type[View]: + from passbook.stages.user_write.stage import UserWriteStageView + + return UserWriteStageView + + def form(self) -> Type[ModelForm]: + from passbook.stages.user_write.forms import UserWriteStageForm + + return UserWriteStageForm def __str__(self): return f"User Write Stage {self.name}" From c9663a08dade5b14fcc9a1ddb68637ccc2da8dc9 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 20 Jul 2020 16:33:34 +0200 Subject: [PATCH 7/9] flows: update work with new stages --- passbook/flows/models.py | 15 ++++++++++----- passbook/flows/views.py | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/passbook/flows/models.py b/passbook/flows/models.py index aa1523722..9eece7308 100644 --- a/passbook/flows/models.py +++ b/passbook/flows/models.py @@ -3,13 +3,13 @@ from typing import TYPE_CHECKING, Optional, Type from uuid import uuid4 from django.db import models +from django.forms import ModelForm from django.http import HttpRequest from django.utils.translation import gettext_lazy as _ from model_utils.managers import InheritanceManager from structlog import get_logger from passbook.core.types import UIUserSettings -from passbook.lib.utils.reflection import class_to_path from passbook.policies.models import PolicyBindingModel if TYPE_CHECKING: @@ -47,8 +47,14 @@ class Stage(models.Model): name = models.TextField() objects = InheritanceManager() - type = "" - form = "" + + def type(self) -> Type["StageView"]: + """Return StageView class that implements logic for this stage""" + raise NotImplementedError + + def form(self) -> Type[ModelForm]: + """Return Form class used to edit this object""" + raise NotImplementedError @property def ui_user_settings(self) -> Optional[UIUserSettings]: @@ -62,9 +68,8 @@ class Stage(models.Model): def in_memory_stage(view: Type["StageView"]) -> Stage: """Creates an in-memory stage instance, based on a `_type` as view.""" - class_path = class_to_path(view) stage = Stage() - stage.type = class_path + setattr(stage, "type", lambda self: view) return stage diff --git a/passbook/flows/views.py b/passbook/flows/views.py index c01ede83b..f2c5e1622 100644 --- a/passbook/flows/views.py +++ b/passbook/flows/views.py @@ -21,7 +21,7 @@ from passbook.core.views.utils import PermissionDeniedView from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException from passbook.flows.models import Flow, FlowDesignation, Stage from passbook.flows.planner import FlowPlan, FlowPlanner -from passbook.lib.utils.reflection import class_to_path, path_to_class +from passbook.lib.utils.reflection import class_to_path from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs from passbook.lib.views import bad_request_message @@ -94,7 +94,7 @@ class FlowExecutorView(View): if not self.current_stage: LOGGER.debug("f(exec): no more stages, flow is done.") return self._flow_done() - stage_cls = path_to_class(self.current_stage.type) + stage_cls = self.current_stage.type() self.current_stage_view = stage_cls(self) self.current_stage_view.args = self.args self.current_stage_view.kwargs = self.kwargs From 4040eb9619a9beb8ae6bc93f49c0170e4f198f15 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 20 Jul 2020 16:43:30 +0200 Subject: [PATCH 8/9] *: remove path-based import from all PropertyMappings --- passbook/providers/saml/models.py | 5 ++++- passbook/sources/ldap/models.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/passbook/providers/saml/models.py b/passbook/providers/saml/models.py index 2e7bd68f4..b98c67fae 100644 --- a/passbook/providers/saml/models.py +++ b/passbook/providers/saml/models.py @@ -147,7 +147,10 @@ class SAMLPropertyMapping(PropertyMapping): saml_name = models.TextField(verbose_name="SAML Name") friendly_name = models.TextField(default=None, blank=True, null=True) - form = "passbook.providers.saml.forms.SAMLPropertyMappingForm" + def form(self) -> Type[ModelForm]: + from passbook.providers.saml.forms import SAMLPropertyMappingForm + + return SAMLPropertyMappingForm def __str__(self): return f"SAML Property Mapping {self.saml_name}" diff --git a/passbook/sources/ldap/models.py b/passbook/sources/ldap/models.py index b4581d11d..4fc1ffc3c 100644 --- a/passbook/sources/ldap/models.py +++ b/passbook/sources/ldap/models.py @@ -89,7 +89,10 @@ class LDAPPropertyMapping(PropertyMapping): object_field = models.TextField() - form = "passbook.sources.ldap.forms.LDAPPropertyMappingForm" + def form(self) -> Type[ModelForm]: + from passbook.sources.ldap.forms import LDAPPropertyMappingForm + + return LDAPPropertyMappingForm def __str__(self): return f"LDAP Property Mapping {self.expression} -> {self.object_field}" From 88029a43355d73011b9fd078231fd932cfa1e2a6 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Mon, 20 Jul 2020 16:55:55 +0200 Subject: [PATCH 9/9] admin: update to work with new form --- passbook/admin/views/utils.py | 8 +++----- passbook/flows/models.py | 8 +++++++- passbook/root/settings.py | 2 ++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/passbook/admin/views/utils.py b/passbook/admin/views/utils.py index 1c2f5e74b..22e984a64 100644 --- a/passbook/admin/views/utils.py +++ b/passbook/admin/views/utils.py @@ -6,7 +6,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.http import Http404 from django.views.generic import DeleteView, ListView, UpdateView -from passbook.lib.utils.reflection import all_subclasses, path_to_class +from passbook.lib.utils.reflection import all_subclasses from passbook.lib.views import CreateAssignPermView @@ -40,7 +40,7 @@ class InheritanceCreateView(CreateAssignPermView): ) except StopIteration as exc: raise Http404 from exc - return path_to_class(model.form) + return model.form(model) def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: kwargs = super().get_context_data(**kwargs) @@ -61,9 +61,7 @@ class InheritanceUpdateView(UpdateView): return kwargs def get_form_class(self): - form_class_path = self.get_object().form - form_class = path_to_class(form_class_path) - return form_class + return self.get_object().form() def get_object(self, queryset=None): return ( diff --git a/passbook/flows/models.py b/passbook/flows/models.py index 9eece7308..06338e596 100644 --- a/passbook/flows/models.py +++ b/passbook/flows/models.py @@ -50,6 +50,9 @@ class Stage(models.Model): def type(self) -> Type["StageView"]: """Return StageView class that implements logic for this stage""" + # This is a bit of a workaround, since we can't set class methods with setattr + if hasattr(self, "__in_memory_type"): + return getattr(self, "__in_memory_type") raise NotImplementedError def form(self) -> Type[ModelForm]: @@ -69,7 +72,10 @@ class Stage(models.Model): def in_memory_stage(view: Type["StageView"]) -> Stage: """Creates an in-memory stage instance, based on a `_type` as view.""" stage = Stage() - setattr(stage, "type", lambda self: view) + # Because we can't pickle a locally generated function, + # we set the view as a separate property and reference a generic function + # that returns that member + setattr(stage, "__in_memory_type", view) return stage diff --git a/passbook/root/settings.py b/passbook/root/settings.py index 77e3aa9e9..0149abdd9 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -325,6 +325,8 @@ LOG_PRE_CHAIN = [ structlog.stdlib.add_log_level, structlog.stdlib.add_logger_name, structlog.processors.TimeStamper(), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, ] LOGGING = {