stages/prompt: add prompt stage: dynamically created forms based on database
This commit is contained in:
0
passbook/stages/prompt/__init__.py
Normal file
0
passbook/stages/prompt/__init__.py
Normal file
48
passbook/stages/prompt/api.py
Normal file
48
passbook/stages/prompt/api.py
Normal 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
|
||||
10
passbook/stages/prompt/apps.py
Normal file
10
passbook/stages/prompt/apps.py
Normal 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"
|
||||
29
passbook/stages/prompt/forms.py
Normal file
29
passbook/stages/prompt/forms.py
Normal 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
|
||||
76
passbook/stages/prompt/migrations/0001_initial.py
Normal file
76
passbook/stages/prompt/migrations/0001_initial.py
Normal 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",),
|
||||
),
|
||||
]
|
||||
0
passbook/stages/prompt/migrations/__init__.py
Normal file
0
passbook/stages/prompt/migrations/__init__.py
Normal file
75
passbook/stages/prompt/models.py
Normal file
75
passbook/stages/prompt/models.py
Normal 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")
|
||||
35
passbook/stages/prompt/stage.py
Normal file
35
passbook/stages/prompt/stage.py
Normal 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()
|
||||
Reference in New Issue
Block a user