stages/prompt: add prompt stage: dynamically created forms based on database

This commit is contained in:
Jens Langhammer
2020-05-10 16:20:17 +02:00
parent 9def45c8d7
commit 4315d1a03c
14 changed files with 303 additions and 311 deletions

View File

View File

@ -0,0 +1,48 @@
"""Prompt Stage API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.stages.prompt.models import Prompt, PromptStage
class PromptStageSerializer(ModelSerializer):
"""PromptStage Serializer"""
class Meta:
model = PromptStage
fields = [
"pk",
"name",
"fields",
]
class PromptStageViewSet(ModelViewSet):
"""PromptStage Viewset"""
queryset = PromptStage.objects.all()
serializer_class = PromptStageSerializer
class PromptSerializer(ModelSerializer):
"""Prompt Serializer"""
class Meta:
model = Prompt
fields = [
"pk",
"field_key",
"label",
"type",
"required",
"placeholder",
]
class PromptViewSet(ModelViewSet):
"""Prompt Viewset"""
queryset = Prompt.objects.all()
serializer_class = PromptSerializer

View File

@ -0,0 +1,10 @@
"""passbook prompt stage app config"""
from django.apps import AppConfig
class PassbookStagPromptConfig(AppConfig):
"""passbook prompt stage config"""
name = "passbook.stages.prompt"
label = "passbook_stages_prompt"
verbose_name = "passbook Stages.Prompt"

View File

@ -0,0 +1,29 @@
"""Prompt forms"""
from django import forms
from passbook.stages.prompt.models import Prompt, PromptStage
class PromptStageForm(forms.ModelForm):
"""Form to create/edit Prompt Stage instances"""
class Meta:
model = PromptStage
fields = ["name", "fields"]
widgets = {
"name": forms.TextInput(),
}
class PromptForm(forms.Form):
"""Dynamically created form based on PromptStage"""
stage: PromptStage
def __init__(self, stage: PromptStage, *args, **kwargs):
self.stage = stage
super().__init__(*args, **kwargs)
for field in self.stage.fields.all():
field: Prompt
self.fields[field.field_key] = field.field

View File

@ -0,0 +1,76 @@
# Generated by Django 3.0.5 on 2020-05-10 14:03
import uuid
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="Prompt",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"field_key",
models.SlugField(
help_text="Name of the form field, also used to store the value"
),
),
("label", models.TextField()),
(
"type",
models.CharField(
choices=[
("text", "Text"),
("e-mail", "Email"),
("password", "Password"),
("number", "Number"),
],
max_length=100,
),
),
("required", models.BooleanField(default=True)),
("placeholder", models.TextField()),
],
options={"abstract": False,},
),
migrations.CreateModel(
name="PromptStage",
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",
),
),
("fields", models.ManyToManyField(to="passbook_stages_prompt.Prompt")),
],
options={
"verbose_name": "Prompt Stage",
"verbose_name_plural": "Prompt Stages",
},
bases=("passbook_flows.stage",),
),
]

View File

@ -0,0 +1,75 @@
"""prompt models"""
from django import forms
from django.db import models
from django.utils.translation import gettext_lazy as _
from passbook.flows.models import Stage
from passbook.lib.models import UUIDModel
class FieldTypes(models.TextChoices):
"""Field types an Prompt can be"""
TEXT = "text"
EMAIL = "e-mail"
PASSWORD = "password" # noqa # nosec
NUMBER = "number"
class Prompt(UUIDModel):
"""Single Prompt, part of a prompt stage."""
field_key = models.SlugField(
help_text=_("Name of the form field, also used to store the value")
)
label = models.TextField()
type = models.CharField(max_length=100, choices=FieldTypes.choices)
required = models.BooleanField(default=True)
placeholder = models.TextField()
@property
def field(self):
"""Return instantiated form input field"""
attrs = {"placeholder": _(self.placeholder)}
if self.type == FieldTypes.TEXT:
return forms.CharField(
label=_(self.label),
widget=forms.TextInput(attrs=attrs),
required=self.required,
)
if self.type == FieldTypes.EMAIL:
return forms.EmailField(
label=_(self.label),
widget=forms.TextInput(attrs=attrs),
required=self.required,
)
if self.type == FieldTypes.PASSWORD:
return forms.CharField(
label=_(self.label),
widget=forms.PasswordInput(attrs=attrs),
required=self.required,
)
if self.type == FieldTypes.NUMBER:
return forms.IntegerField(
label=_(self.label),
widget=forms.NumberInput(attrs=attrs),
requred=self.required,
)
raise ValueError
class PromptStage(Stage):
"""Prompt Stage, pointing to multiple prompts"""
fields = models.ManyToManyField(Prompt)
type = "passbook.stages.prompt.stage.PromptStageView"
form = "passbook.stages.prompt.forms.PromptStageForm"
def __str__(self):
return f"Prompt Stage {self.name}"
class Meta:
verbose_name = _("Prompt Stage")
verbose_name_plural = _("Prompt Stages")

View File

@ -0,0 +1,35 @@
"""Enrollment Stage Logic"""
from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from structlog import get_logger
from passbook.flows.stage import AuthenticationStage
from passbook.stages.prompt.forms import PromptForm
LOGGER = get_logger()
PLAN_CONTEXT_PROMPT = "prompt_data"
class EnrollmentStageView(FormView, AuthenticationStage):
"""Enrollment Stage, save form data in plan context."""
template_name = "login/form.html"
form_class = PromptForm
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["title"] = _(self.executor.current_stage.name)
return ctx
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["stage"] = self.executor.current_stage
return kwargs
def form_valid(self, form: PromptForm) -> HttpResponse:
"""Form data is valid"""
if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = {}
self.executor.plan.context[PLAN_CONTEXT_PROMPT].update(form.cleaned_data)
return self.executor.stage_ok()