diff --git a/passbook/admin/views/utils.py b/passbook/admin/views/utils.py index e8f2fd18d..45bc21df0 100644 --- a/passbook/admin/views/utils.py +++ b/passbook/admin/views/utils.py @@ -64,7 +64,7 @@ class InheritanceUpdateView(UpdateView): return kwargs def get_form_class(self): - return self.get_object().form() + return self.get_object().form def get_object(self, queryset=None): return ( diff --git a/passbook/core/models.py b/passbook/core/models.py index 0bf89dde0..9347183fa 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -136,6 +136,7 @@ class Provider(models.Model): Can return None for providers that are not URL-based""" return None + @property def form(self) -> Type[ModelForm]: """Return Form class used to edit this object""" raise NotImplementedError @@ -220,6 +221,7 @@ class Source(PolicyBindingModel): objects = InheritanceManager() + @property def form(self) -> Type[ModelForm]: """Return Form class used to edit this object""" raise NotImplementedError @@ -321,6 +323,7 @@ class PropertyMapping(models.Model): objects = InheritanceManager() + @property def form(self) -> Type[ModelForm]: """Return Form class used to edit this object""" raise NotImplementedError diff --git a/passbook/core/views/error.py b/passbook/core/views/error.py index 4d498e36d..ad4eb9840 100644 --- a/passbook/core/views/error.py +++ b/passbook/core/views/error.py @@ -62,6 +62,6 @@ class ServerErrorView(TemplateView): template_name = "error/generic.html" # pylint: disable=useless-super-delegation - def dispatch(self, *args, **kwargs): + def dispatch(self, *args, **kwargs): # pragma: no cover """Little wrapper so django accepts this function""" return super().dispatch(*args, **kwargs) diff --git a/passbook/flows/models.py b/passbook/flows/models.py index dc152e252..b802d441b 100644 --- a/passbook/flows/models.py +++ b/passbook/flows/models.py @@ -50,6 +50,7 @@ class Stage(SerializerModel): objects = InheritanceManager() + @property 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 @@ -57,6 +58,7 @@ class Stage(SerializerModel): return getattr(self, "__in_memory_type") raise NotImplementedError + @property def form(self) -> Type[ModelForm]: """Return Form class used to edit this object""" raise NotImplementedError diff --git a/passbook/flows/tests/test_models.py b/passbook/flows/tests/test_models.py new file mode 100644 index 000000000..e466f2fb0 --- /dev/null +++ b/passbook/flows/tests/test_models.py @@ -0,0 +1,31 @@ +"""flow model tests""" +from typing import Callable, Type + +from django.forms import ModelForm +from django.test import TestCase + +from passbook.flows.models import Stage +from passbook.flows.stage import StageView + + +class TestStageProperties(TestCase): + """Generic model properties tests""" + + +def stage_tester_factory(model: Type[Stage]) -> Callable: + """Test a form""" + + def tester(self: TestStageProperties): + model_inst = model() + self.assertTrue(issubclass(model_inst.form, ModelForm)) + self.assertTrue(issubclass(model_inst.type, StageView)) + + return tester + + +for stage_type in Stage.__subclasses__(): + setattr( + TestStageProperties, + f"test_stage_{stage_type.__name__}", + stage_tester_factory(stage_type), + ) diff --git a/passbook/flows/views.py b/passbook/flows/views.py index b35dc4f8a..5ad3eb644 100644 --- a/passbook/flows/views.py +++ b/passbook/flows/views.py @@ -96,7 +96,7 @@ class FlowExecutorView(View): current_stage=self.current_stage, flow_slug=self.flow.slug, ) - stage_cls = 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 diff --git a/passbook/lib/tests.py b/passbook/lib/tests.py new file mode 100644 index 000000000..e8767de2a --- /dev/null +++ b/passbook/lib/tests.py @@ -0,0 +1,30 @@ +"""base model tests""" +from typing import Callable, Type + +from django.test import TestCase +from rest_framework.serializers import BaseSerializer + +from passbook.flows.models import Stage +from passbook.lib.models import SerializerModel +from passbook.lib.utils.reflection import all_subclasses + + +class TestModels(TestCase): + """Generic model properties tests""" + + +def model_tester_factory(test_model: Type[Stage]) -> Callable: + """Test a form""" + + def tester(self: TestModels): + model_inst = test_model() + try: + self.assertTrue(issubclass(model_inst.serializer, BaseSerializer)) + except NotImplementedError: + pass + + return tester + + +for model in all_subclasses(SerializerModel): + setattr(TestModels, f"test_model_{model.__name__}", model_tester_factory(model)) diff --git a/passbook/policies/dummy/models.py b/passbook/policies/dummy/models.py index 75dcc109a..8ff36306c 100644 --- a/passbook/policies/dummy/models.py +++ b/passbook/policies/dummy/models.py @@ -31,6 +31,7 @@ class DummyPolicy(Policy): return DummyPolicySerializer + @property def form(self) -> Type[ModelForm]: from passbook.policies.dummy.forms import DummyPolicyForm diff --git a/passbook/policies/engine.py b/passbook/policies/engine.py index cf48d706f..c02e7408d 100644 --- a/passbook/policies/engine.py +++ b/passbook/policies/engine.py @@ -50,7 +50,7 @@ class PolicyEngine: def __init__( self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None ): - if not isinstance(pbm, PolicyBindingModel): + if not isinstance(pbm, PolicyBindingModel): # pragma: no cover raise ValueError(f"{pbm} is not instance of PolicyBindingModel") self.__pbm = pbm self.request = PolicyRequest(user) diff --git a/passbook/policies/expiry/models.py b/passbook/policies/expiry/models.py index 96fed48aa..96b59269b 100644 --- a/passbook/policies/expiry/models.py +++ b/passbook/policies/expiry/models.py @@ -28,6 +28,7 @@ class PasswordExpiryPolicy(Policy): return PasswordExpiryPolicySerializer + @property def form(self) -> Type[ModelForm]: from passbook.policies.expiry.forms import PasswordExpiryPolicyForm diff --git a/passbook/policies/expression/models.py b/passbook/policies/expression/models.py index e88c6d5af..6bdce9fe3 100644 --- a/passbook/policies/expression/models.py +++ b/passbook/policies/expression/models.py @@ -22,6 +22,7 @@ class ExpressionPolicy(Policy): return ExpressionPolicySerializer + @property def form(self) -> Type[ModelForm]: from passbook.policies.expression.forms import ExpressionPolicyForm diff --git a/passbook/policies/group_membership/models.py b/passbook/policies/group_membership/models.py index 55726be1a..c163aa68a 100644 --- a/passbook/policies/group_membership/models.py +++ b/passbook/policies/group_membership/models.py @@ -24,6 +24,7 @@ class GroupMembershipPolicy(Policy): return GroupMembershipPolicySerializer + @property def form(self) -> Type[ModelForm]: from passbook.policies.group_membership.forms import GroupMembershipPolicyForm diff --git a/passbook/policies/hibp/models.py b/passbook/policies/hibp/models.py index 51f759c6e..56367a1da 100644 --- a/passbook/policies/hibp/models.py +++ b/passbook/policies/hibp/models.py @@ -34,6 +34,7 @@ class HaveIBeenPwendPolicy(Policy): return HaveIBeenPwendPolicySerializer + @property def form(self) -> Type[ModelForm]: from passbook.policies.hibp.forms import HaveIBeenPwnedPolicyForm diff --git a/passbook/policies/models.py b/passbook/policies/models.py index 99eb43495..b92924745 100644 --- a/passbook/policies/models.py +++ b/passbook/policies/models.py @@ -83,14 +83,15 @@ class Policy(SerializerModel, CreatedUpdatedModel): objects = InheritanceAutoManager() + @property def form(self) -> Type[ModelForm]: """Return Form class used to edit this object""" raise NotImplementedError def __str__(self): - return f"Policy {self.name}" + return f"{self.__class__.__name__} {self.name}" - def passes(self, request: PolicyRequest) -> PolicyResult: + def passes(self, request: PolicyRequest) -> PolicyResult: # pragma: no cover """Check if user instance passes this policy""" raise PolicyException() diff --git a/passbook/policies/password/models.py b/passbook/policies/password/models.py index 2c0981434..f2bc29b96 100644 --- a/passbook/policies/password/models.py +++ b/passbook/policies/password/models.py @@ -37,6 +37,7 @@ class PasswordPolicy(Policy): return PasswordPolicySerializer + @property def form(self) -> Type[ModelForm]: from passbook.policies.password.forms import PasswordPolicyForm diff --git a/passbook/policies/reputation/models.py b/passbook/policies/reputation/models.py index a703fffd1..37e7ecbac 100644 --- a/passbook/policies/reputation/models.py +++ b/passbook/policies/reputation/models.py @@ -29,6 +29,7 @@ class ReputationPolicy(Policy): return ReputationPolicySerializer + @property def form(self) -> Type[ModelForm]: from passbook.policies.reputation.forms import ReputationPolicyForm diff --git a/passbook/policies/tests/__init__.py b/passbook/policies/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/policies/tests.py b/passbook/policies/tests/test_engine.py similarity index 100% rename from passbook/policies/tests.py rename to passbook/policies/tests/test_engine.py diff --git a/passbook/policies/tests/test_models.py b/passbook/policies/tests/test_models.py new file mode 100644 index 000000000..1a1e6742a --- /dev/null +++ b/passbook/policies/tests/test_models.py @@ -0,0 +1,30 @@ +"""flow model tests""" +from typing import Callable, Type + +from django.forms import ModelForm +from django.test import TestCase + +from passbook.lib.utils.reflection import all_subclasses +from passbook.policies.models import Policy + + +class TestPolicyProperties(TestCase): + """Generic model properties tests""" + + +def policy_tester_factory(model: Type[Policy]) -> Callable: + """Test a form""" + + def tester(self: TestPolicyProperties): + model_inst = model() + self.assertTrue(issubclass(model_inst.form, ModelForm)) + + return tester + + +for policy_type in all_subclasses(Policy): + setattr( + TestPolicyProperties, + f"test_policy_{policy_type.__name__}", + policy_tester_factory(policy_type), + ) diff --git a/passbook/providers/oauth2/models.py b/passbook/providers/oauth2/models.py index 441006a11..86d934e89 100644 --- a/passbook/providers/oauth2/models.py +++ b/passbook/providers/oauth2/models.py @@ -102,6 +102,7 @@ class ScopeMapping(PropertyMapping): ), ) + @property def form(self) -> Type[ModelForm]: from passbook.providers.oauth2.forms import ScopeMappingForm @@ -264,6 +265,7 @@ class OAuth2Provider(Provider): launch_url = urlparse(main_url) return main_url.replace(launch_url.path, "") + @property def form(self) -> Type[ModelForm]: from passbook.providers.oauth2.forms import OAuth2ProviderForm diff --git a/passbook/providers/proxy/models.py b/passbook/providers/proxy/models.py index 2ea358efa..ac01b6572 100644 --- a/passbook/providers/proxy/models.py +++ b/passbook/providers/proxy/models.py @@ -67,6 +67,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider): cookie_secret = models.TextField(default=get_cookie_secret) + @property def form(self) -> Type[ModelForm]: from passbook.providers.proxy.forms import ProxyProviderForm diff --git a/passbook/providers/proxy/tests.py b/passbook/providers/proxy/tests.py new file mode 100644 index 000000000..6168e9e5f --- /dev/null +++ b/passbook/providers/proxy/tests.py @@ -0,0 +1,32 @@ +"""Test Controllers""" +import yaml +from django.test import TestCase + +from passbook.flows.models import Flow +from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType +from passbook.providers.proxy.controllers.kubernetes import KubernetesController +from passbook.providers.proxy.models import ProxyProvider + + +class TestControllers(TestCase): + """Test Controllers""" + + def test_kubernetes_controller(self): + """Test Kubernetes Controller""" + provider: ProxyProvider = ProxyProvider.objects.create( + name="test", + internal_host="http://localhost", + external_host="http://localhost", + authorization_flow=Flow.objects.first(), + ) + outpost: Outpost = Outpost.objects.create( + name="test", + type=OutpostType.PROXY, + deployment_type=OutpostDeploymentType.CUSTOM, + ) + outpost.providers.add(provider) + outpost.save() + + controller = KubernetesController(outpost.pk) + manifest = controller.get_static_deployment() + self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 3) diff --git a/passbook/providers/saml/models.py b/passbook/providers/saml/models.py index ce680c370..f7a530091 100644 --- a/passbook/providers/saml/models.py +++ b/passbook/providers/saml/models.py @@ -109,6 +109,7 @@ class SAMLProvider(Provider): launch_url = urlparse(self.acs_url) return self.acs_url.replace(launch_url.path, "") + @property def form(self) -> Type[ModelForm]: from passbook.providers.saml.forms import SAMLProviderForm @@ -154,6 +155,7 @@ class SAMLPropertyMapping(PropertyMapping): saml_name = models.TextField(verbose_name="SAML Name") friendly_name = models.TextField(default=None, blank=True, null=True) + @property def form(self) -> Type[ModelForm]: from passbook.providers.saml.forms import SAMLPropertyMappingForm diff --git a/passbook/sources/ldap/models.py b/passbook/sources/ldap/models.py index 20df911f6..a53308348 100644 --- a/passbook/sources/ldap/models.py +++ b/passbook/sources/ldap/models.py @@ -67,6 +67,7 @@ class LDAPSource(Source): Group, blank=True, null=True, default=None, on_delete=models.SET_DEFAULT ) + @property def form(self) -> Type[ModelForm]: from passbook.sources.ldap.forms import LDAPSourceForm @@ -116,6 +117,7 @@ class LDAPPropertyMapping(PropertyMapping): object_field = models.TextField() + @property def form(self) -> Type[ModelForm]: from passbook.sources.ldap.forms import LDAPPropertyMappingForm diff --git a/passbook/sources/oauth/models.py b/passbook/sources/oauth/models.py index 06d4e42e4..8279b3331 100644 --- a/passbook/sources/oauth/models.py +++ b/passbook/sources/oauth/models.py @@ -40,6 +40,7 @@ class OAuthSource(Source): consumer_key = models.TextField() consumer_secret = models.TextField() + @property def form(self) -> Type[ModelForm]: from passbook.sources.oauth.forms import OAuthSourceForm @@ -83,6 +84,7 @@ class OAuthSource(Source): class GitHubOAuthSource(OAuthSource): """Social Login using GitHub.com or a GitHub-Enterprise Instance.""" + @property def form(self) -> Type[ModelForm]: from passbook.sources.oauth.forms import GitHubOAuthSourceForm @@ -98,6 +100,7 @@ class GitHubOAuthSource(OAuthSource): class TwitterOAuthSource(OAuthSource): """Social Login using Twitter.com""" + @property def form(self) -> Type[ModelForm]: from passbook.sources.oauth.forms import TwitterOAuthSourceForm @@ -113,6 +116,7 @@ class TwitterOAuthSource(OAuthSource): class FacebookOAuthSource(OAuthSource): """Social Login using Facebook.com.""" + @property def form(self) -> Type[ModelForm]: from passbook.sources.oauth.forms import FacebookOAuthSourceForm @@ -128,6 +132,7 @@ class FacebookOAuthSource(OAuthSource): class DiscordOAuthSource(OAuthSource): """Social Login using Discord.""" + @property def form(self) -> Type[ModelForm]: from passbook.sources.oauth.forms import DiscordOAuthSourceForm @@ -143,6 +148,7 @@ class DiscordOAuthSource(OAuthSource): class GoogleOAuthSource(OAuthSource): """Social Login using Google or Gsuite.""" + @property def form(self) -> Type[ModelForm]: from passbook.sources.oauth.forms import GoogleOAuthSourceForm @@ -158,6 +164,7 @@ class GoogleOAuthSource(OAuthSource): class AzureADOAuthSource(OAuthSource): """Social Login using Azure AD.""" + @property def form(self) -> Type[ModelForm]: from passbook.sources.oauth.forms import AzureADOAuthSourceForm @@ -173,6 +180,7 @@ class AzureADOAuthSource(OAuthSource): class OpenIDOAuthSource(OAuthSource): """Login using a Generic OpenID-Connect compliant provider.""" + @property def form(self) -> Type[ModelForm]: from passbook.sources.oauth.forms import OAuthSourceForm diff --git a/passbook/sources/saml/models.py b/passbook/sources/saml/models.py index 9a9568739..756cc24b1 100644 --- a/passbook/sources/saml/models.py +++ b/passbook/sources/saml/models.py @@ -103,6 +103,7 @@ class SAMLSource(Source): on_delete=models.PROTECT, ) + @property def form(self) -> Type[ModelForm]: from passbook.sources.saml.forms import SAMLSourceForm diff --git a/passbook/stages/captcha/models.py b/passbook/stages/captcha/models.py index 58e23ae64..cbe56e26b 100644 --- a/passbook/stages/captcha/models.py +++ b/passbook/stages/captcha/models.py @@ -30,11 +30,13 @@ class CaptchaStage(Stage): return CaptchaStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.captcha.stage import CaptchaStageView return CaptchaStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.captcha.forms import CaptchaStageForm diff --git a/passbook/stages/consent/models.py b/passbook/stages/consent/models.py index c8ef52f78..b74d5b434 100644 --- a/passbook/stages/consent/models.py +++ b/passbook/stages/consent/models.py @@ -44,11 +44,13 @@ class ConsentStage(Stage): return ConsentStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.consent.stage import ConsentStageView return ConsentStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.consent.forms import ConsentStageForm diff --git a/passbook/stages/dummy/models.py b/passbook/stages/dummy/models.py index 839fce0d1..724bd29d8 100644 --- a/passbook/stages/dummy/models.py +++ b/passbook/stages/dummy/models.py @@ -20,11 +20,13 @@ class DummyStage(Stage): return DummyStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.dummy.stage import DummyStageView return DummyStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.dummy.forms import DummyStageForm diff --git a/passbook/stages/email/models.py b/passbook/stages/email/models.py index 624aaba61..54d55e9b9 100644 --- a/passbook/stages/email/models.py +++ b/passbook/stages/email/models.py @@ -51,11 +51,13 @@ class EmailStage(Stage): return EmailStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.email.stage import EmailStageView return EmailStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.email.forms import EmailStageForm diff --git a/passbook/stages/identification/models.py b/passbook/stages/identification/models.py index 4e7e3719c..f4e6dccf5 100644 --- a/passbook/stages/identification/models.py +++ b/passbook/stages/identification/models.py @@ -63,11 +63,13 @@ class IdentificationStage(Stage): return IdentificationStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.identification.stage import IdentificationStageView return IdentificationStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.identification.forms import IdentificationStageForm diff --git a/passbook/stages/invitation/models.py b/passbook/stages/invitation/models.py index c892b526b..7cd08c88f 100644 --- a/passbook/stages/invitation/models.py +++ b/passbook/stages/invitation/models.py @@ -33,11 +33,13 @@ class InvitationStage(Stage): return InvitationStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.invitation.stage import InvitationStageView return InvitationStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.invitation.forms import InvitationStageForm diff --git a/passbook/stages/otp_static/models.py b/passbook/stages/otp_static/models.py index 06fddbed2..00be1706f 100644 --- a/passbook/stages/otp_static/models.py +++ b/passbook/stages/otp_static/models.py @@ -23,11 +23,13 @@ class OTPStaticStage(ConfigurableStage, Stage): return OTPStaticStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.otp_static.stage import OTPStaticStageView return OTPStaticStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.otp_static.forms import OTPStaticStageForm diff --git a/passbook/stages/otp_time/models.py b/passbook/stages/otp_time/models.py index e82b33936..3e3fcdd29 100644 --- a/passbook/stages/otp_time/models.py +++ b/passbook/stages/otp_time/models.py @@ -30,11 +30,13 @@ class OTPTimeStage(ConfigurableStage, Stage): return OTPTimeStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.otp_time.stage import OTPTimeStageView return OTPTimeStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.otp_time.forms import OTPTimeStageForm diff --git a/passbook/stages/otp_validate/models.py b/passbook/stages/otp_validate/models.py index e8d995234..145c79f05 100644 --- a/passbook/stages/otp_validate/models.py +++ b/passbook/stages/otp_validate/models.py @@ -23,11 +23,13 @@ class OTPValidateStage(Stage): return OTPValidateStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.otp_validate.stage import OTPValidateStageView return OTPValidateStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.otp_validate.forms import OTPValidateStageForm diff --git a/passbook/stages/password/models.py b/passbook/stages/password/models.py index 51bf20215..c2303da9b 100644 --- a/passbook/stages/password/models.py +++ b/passbook/stages/password/models.py @@ -38,11 +38,13 @@ class PasswordStage(ConfigurableStage, Stage): return PasswordStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.password.stage import PasswordStageView return PasswordStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.password.forms import PasswordStageForm diff --git a/passbook/stages/prompt/models.py b/passbook/stages/prompt/models.py index 70b55b652..e19f3d44e 100644 --- a/passbook/stages/prompt/models.py +++ b/passbook/stages/prompt/models.py @@ -145,11 +145,13 @@ class PromptStage(Stage): return PromptStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.prompt.stage import PromptStageView return PromptStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.prompt.forms import PromptStageForm diff --git a/passbook/stages/user_delete/models.py b/passbook/stages/user_delete/models.py index c5a896421..b4388019b 100644 --- a/passbook/stages/user_delete/models.py +++ b/passbook/stages/user_delete/models.py @@ -19,11 +19,13 @@ class UserDeleteStage(Stage): return UserDeleteStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.user_delete.stage import UserDeleteStageView return UserDeleteStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.user_delete.forms import UserDeleteStageForm diff --git a/passbook/stages/user_login/models.py b/passbook/stages/user_login/models.py index a8c5a4539..5f500900c 100644 --- a/passbook/stages/user_login/models.py +++ b/passbook/stages/user_login/models.py @@ -27,11 +27,13 @@ class UserLoginStage(Stage): return UserLoginStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.user_login.stage import UserLoginStageView return UserLoginStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.user_login.forms import UserLoginStageForm diff --git a/passbook/stages/user_logout/models.py b/passbook/stages/user_logout/models.py index bd9677511..70df84d77 100644 --- a/passbook/stages/user_logout/models.py +++ b/passbook/stages/user_logout/models.py @@ -18,11 +18,13 @@ class UserLogoutStage(Stage): return UserLogoutStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.user_logout.stage import UserLogoutStageView return UserLogoutStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.user_logout.forms import UserLogoutStageForm diff --git a/passbook/stages/user_write/models.py b/passbook/stages/user_write/models.py index c1ba6be7a..6b73fa96c 100644 --- a/passbook/stages/user_write/models.py +++ b/passbook/stages/user_write/models.py @@ -19,11 +19,13 @@ class UserWriteStage(Stage): return UserWriteStageSerializer + @property def type(self) -> Type[View]: from passbook.stages.user_write.stage import UserWriteStageView return UserWriteStageView + @property def form(self) -> Type[ModelForm]: from passbook.stages.user_write.forms import UserWriteStageForm diff --git a/pytest.ini b/pytest.ini index 56d9dad2f..55c93ff0e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,5 @@ [pytest] DJANGO_SETTINGS_MODULE = passbook.root.settings -# -- recommended but optional: python_files = tests.py test_*.py *_tests.py junit_family = xunit2 addopts = -p no:celery --junitxml=unittest.xml