stages/user_create: add stage to create user after prompts
This commit is contained in:
parent
f6461b08d7
commit
8dc3c49a2f
|
@ -37,6 +37,7 @@ from passbook.stages.identification.api import IdentificationStageViewSet
|
|||
from passbook.stages.otp.api import OTPStageViewSet
|
||||
from passbook.stages.password.api import PasswordStageViewSet
|
||||
from passbook.stages.prompt.api import PromptStageViewSet, PromptViewSet
|
||||
from passbook.stages.user_create.api import UserCreateStageViewSet
|
||||
from passbook.stages.user_login.api import UserLoginStageViewSet
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
@ -85,6 +86,7 @@ router.register("stages/otp", OTPStageViewSet)
|
|||
router.register("stages/password", PasswordStageViewSet)
|
||||
router.register("stages/prompt", PromptStageViewSet)
|
||||
router.register("stages/prompt/prompts", PromptViewSet)
|
||||
router.register("stages/user_create", UserCreateStageViewSet)
|
||||
router.register("stages/user_login", UserLoginStageViewSet)
|
||||
|
||||
router.register("flows", FlowViewSet)
|
||||
|
|
|
@ -107,6 +107,7 @@ INSTALLED_APPS = [
|
|||
"passbook.stages.email.apps.PassbookStageEmailConfig",
|
||||
"passbook.stages.prompt.apps.PassbookStagPromptConfig",
|
||||
"passbook.stages.identification.apps.PassbookStageIdentificationConfig",
|
||||
"passbook.stages.user_create.apps.PassbookStageUserCreateConfig",
|
||||
"passbook.stages.user_login.apps.PassbookStageUserLoginConfig",
|
||||
"passbook.stages.otp.apps.PassbookStageOTPConfig",
|
||||
"passbook.stages.password.apps.PassbookStagePasswordConfig",
|
||||
|
@ -357,7 +358,7 @@ TEST_OUTPUT_VERBOSE = 2
|
|||
TEST_OUTPUT_FILE_NAME = "unittest.xml"
|
||||
|
||||
if any("test" in arg for arg in sys.argv):
|
||||
LOGGING = None
|
||||
# LOGGING = None
|
||||
TEST = True
|
||||
CELERY_TASK_ALWAYS_EAGER = True
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
"""User Create Stage API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.stages.user_create.models import UserCreateStage
|
||||
|
||||
|
||||
class UserCreateStageSerializer(ModelSerializer):
|
||||
"""UserCreateStage Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = UserCreateStage
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
]
|
||||
|
||||
|
||||
class UserCreateStageViewSet(ModelViewSet):
|
||||
"""UserCreateStage Viewset"""
|
||||
|
||||
queryset = UserCreateStage.objects.all()
|
||||
serializer_class = UserCreateStageSerializer
|
|
@ -0,0 +1,10 @@
|
|||
"""passbook create stage app config"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookStageUserCreateConfig(AppConfig):
|
||||
"""passbook create stage config"""
|
||||
|
||||
name = "passbook.stages.user_create"
|
||||
label = "passbook_stages_user_create"
|
||||
verbose_name = "passbook Stages.User Create"
|
|
@ -0,0 +1,16 @@
|
|||
"""passbook flows create forms"""
|
||||
from django import forms
|
||||
|
||||
from passbook.stages.user_create.models import UserCreateStage
|
||||
|
||||
|
||||
class UserCreateStageForm(forms.ModelForm):
|
||||
"""Form to create/edit UserCreateStage instances"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = UserCreateStage
|
||||
fields = ["name"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 3.0.5 on 2020-05-10 14:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("passbook_flows", "0003_auto_20200509_1258"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="UserCreateStage",
|
||||
fields=[
|
||||
(
|
||||
"stage_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_flows.Stage",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "User Create Stage",
|
||||
"verbose_name_plural": "User Create Stages",
|
||||
},
|
||||
bases=("passbook_flows.stage",),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
"""create stage models"""
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.flows.models import Stage
|
||||
|
||||
|
||||
class UserCreateStage(Stage):
|
||||
"""Create stage, create a user from saved data."""
|
||||
|
||||
type = "passbook.stages.user_create.stage.UserCreateStageView"
|
||||
form = "passbook.stages.user_create.forms.UserCreateStageForm"
|
||||
|
||||
def __str__(self):
|
||||
return f"User Create Stage {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("User Create Stage")
|
||||
verbose_name_plural = _("User Create Stages")
|
|
@ -0,0 +1,39 @@
|
|||
"""Create stage logic"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from passbook.flows.stage import AuthenticationStage
|
||||
from passbook.lib.utils.reflection import class_to_path
|
||||
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class UserCreateStageView(AuthenticationStage):
|
||||
"""Finalise Enrollment flow by creating a user object."""
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
|
||||
message = _("No Pending data.")
|
||||
messages.error(request, message)
|
||||
LOGGER.debug(message)
|
||||
return self.executor.stage_invalid()
|
||||
data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
|
||||
user = User.objects.create_user(**data)
|
||||
# Set created user as pending_user, so this can be chained with user_login
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = user
|
||||
self.executor.plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = class_to_path(
|
||||
ModelBackend
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Created user",
|
||||
user=self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
|
||||
flow_slug=self.executor.flow.slug,
|
||||
)
|
||||
return self.executor.stage_ok()
|
|
@ -0,0 +1,73 @@
|
|||
"""create tests"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.flows.planner import FlowPlan
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
from passbook.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from passbook.stages.user_create.models import UserCreateStage
|
||||
|
||||
|
||||
class TestUserCreateStage(TestCase):
|
||||
"""Create tests"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client = Client()
|
||||
|
||||
self.password = "".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(8)
|
||||
)
|
||||
self.flow = Flow.objects.create(
|
||||
name="test-create",
|
||||
slug="test-create",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
self.stage = UserCreateStage.objects.create(name="create")
|
||||
FlowStageBinding.objects.create(flow=self.flow, stage=self.stage, order=2)
|
||||
|
||||
def test_valid_create(self):
|
||||
"""Test creation of user"""
|
||||
plan = FlowPlan(stages=[self.stage])
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
"username": "test-user",
|
||||
"name": "name",
|
||||
"email": "test@beryju.org",
|
||||
"password": self.password,
|
||||
}
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(
|
||||
User.objects.filter(
|
||||
username=plan.context[PLAN_CONTEXT_PROMPT]["username"]
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_without_data(self):
|
||||
"""Test without data results in error"""
|
||||
plan = FlowPlan(stages=[self.stage])
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("passbook_flows:denied"))
|
Reference in New Issue