stages/user_create: add stage to create user after prompts
This commit is contained in:
		@ -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
									
								
								passbook/stages/user_create/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/stages/user_create/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										24
									
								
								passbook/stages/user_create/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								passbook/stages/user_create/api.py
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
							
								
								
									
										10
									
								
								passbook/stages/user_create/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								passbook/stages/user_create/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -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"
 | 
			
		||||
							
								
								
									
										16
									
								
								passbook/stages/user_create/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								passbook/stages/user_create/forms.py
									
									
									
									
									
										Normal file
									
								
							@ -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(),
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										37
									
								
								passbook/stages/user_create/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								passbook/stages/user_create/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@ -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
									
								
								passbook/stages/user_create/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/stages/user_create/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										19
									
								
								passbook/stages/user_create/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								passbook/stages/user_create/models.py
									
									
									
									
									
										Normal file
									
								
							@ -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")
 | 
			
		||||
							
								
								
									
										39
									
								
								passbook/stages/user_create/stage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								passbook/stages/user_create/stage.py
									
									
									
									
									
										Normal file
									
								
							@ -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()
 | 
			
		||||
							
								
								
									
										73
									
								
								passbook/stages/user_create/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								passbook/stages/user_create/tests.py
									
									
									
									
									
										Normal file
									
								
							@ -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
	
	Block a user