policies: change .form() and .serializer() to properties, add tests
This commit is contained in:
parent
5da4ff4ff1
commit
9724ded194
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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))
|
|
@ -31,6 +31,7 @@ class DummyPolicy(Policy):
|
|||
|
||||
return DummyPolicySerializer
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.policies.dummy.forms import DummyPolicyForm
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -28,6 +28,7 @@ class PasswordExpiryPolicy(Policy):
|
|||
|
||||
return PasswordExpiryPolicySerializer
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.policies.expiry.forms import PasswordExpiryPolicyForm
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ class ExpressionPolicy(Policy):
|
|||
|
||||
return ExpressionPolicySerializer
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.policies.expression.forms import ExpressionPolicyForm
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ class GroupMembershipPolicy(Policy):
|
|||
|
||||
return GroupMembershipPolicySerializer
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.policies.group_membership.forms import GroupMembershipPolicyForm
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ class HaveIBeenPwendPolicy(Policy):
|
|||
|
||||
return HaveIBeenPwendPolicySerializer
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.policies.hibp.forms import HaveIBeenPwnedPolicyForm
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ class PasswordPolicy(Policy):
|
|||
|
||||
return PasswordPolicySerializer
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.policies.password.forms import PasswordPolicyForm
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ class ReputationPolicy(Policy):
|
|||
|
||||
return ReputationPolicySerializer
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.policies.reputation.forms import ReputationPolicyForm
|
||||
|
||||
|
|
|
@ -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),
|
||||
)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ class SAMLSource(Source):
|
|||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
@property
|
||||
def form(self) -> Type[ModelForm]:
|
||||
from passbook.sources.saml.forms import SAMLSourceForm
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in New Issue