Merge branch 'master' into e2e
This commit is contained in:
commit
467b95cf02
|
@ -29,6 +29,7 @@
|
||||||
<th role="columnheader" scope="col">{% trans 'Field' %}</th>
|
<th role="columnheader" scope="col">{% trans 'Field' %}</th>
|
||||||
<th role="columnheader" scope="col">{% trans 'Label' %}</th>
|
<th role="columnheader" scope="col">{% trans 'Label' %}</th>
|
||||||
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
|
<th role="columnheader" scope="col">{% trans 'Type' %}</th>
|
||||||
|
<th role="columnheader" scope="col">{% trans 'Order' %}</th>
|
||||||
<th role="columnheader" scope="col">{% trans 'Flows' %}</th>
|
<th role="columnheader" scope="col">{% trans 'Flows' %}</th>
|
||||||
<th role="cell"></th>
|
<th role="cell"></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -51,6 +52,11 @@
|
||||||
{{ prompt.type }}
|
{{ prompt.type }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td role="cell">
|
||||||
|
<div>
|
||||||
|
{{ prompt.order }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td role="cell">
|
<td role="cell">
|
||||||
<ul>
|
<ul>
|
||||||
{% for flow in prompt.flow_set.all %}
|
{% for flow in prompt.flow_set.all %}
|
||||||
|
|
|
@ -20,7 +20,7 @@ class PromptListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||||
|
|
||||||
model = Prompt
|
model = Prompt
|
||||||
permission_required = "passbook_stages_prompt.view_prompt"
|
permission_required = "passbook_stages_prompt.view_prompt"
|
||||||
ordering = "field_key"
|
ordering = "order"
|
||||||
paginate_by = 40
|
paginate_by = 40
|
||||||
template_name = "administration/stage_prompt/list.html"
|
template_name = "administration/stage_prompt/list.html"
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,6 @@ class FlowPlanner:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"f(plan): Taking plan from cache", flow=self.flow, key=cached_plan_key
|
"f(plan): Taking plan from cache", flow=self.flow, key=cached_plan_key
|
||||||
)
|
)
|
||||||
LOGGER.debug(cached_plan)
|
|
||||||
return cached_plan
|
return cached_plan
|
||||||
plan = self._build_plan(user, request, default_context)
|
plan = self._build_plan(user, request, default_context)
|
||||||
cache.set(cache_key(self.flow, user), plan)
|
cache.set(cache_key(self.flow, user), plan)
|
||||||
|
|
|
@ -138,7 +138,7 @@ const loadFormCode = () => {
|
||||||
newScript.src = script.src;
|
newScript.src = script.src;
|
||||||
document.head.appendChild(newScript);
|
document.head.appendChild(newScript);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
const setFormSubmitHandlers = () => {
|
const setFormSubmitHandlers = () => {
|
||||||
document.querySelectorAll("#flow-body form").forEach(form => {
|
document.querySelectorAll("#flow-body form").forEach(form => {
|
||||||
console.log(`Setting action for form ${form}`);
|
console.log(`Setting action for form ${form}`);
|
||||||
|
|
|
@ -38,13 +38,13 @@ class IdentificationStageView(FormView, StageView):
|
||||||
enrollment_flow = self.executor.flow.related_flow(FlowDesignation.ENROLLMENT)
|
enrollment_flow = self.executor.flow.related_flow(FlowDesignation.ENROLLMENT)
|
||||||
if enrollment_flow:
|
if enrollment_flow:
|
||||||
kwargs["enroll_url"] = reverse(
|
kwargs["enroll_url"] = reverse(
|
||||||
"passbook_flows:flow-executor",
|
"passbook_flows:flow-executor-shell",
|
||||||
kwargs={"flow_slug": enrollment_flow.slug},
|
kwargs={"flow_slug": enrollment_flow.slug},
|
||||||
)
|
)
|
||||||
recovery_flow = self.executor.flow.related_flow(FlowDesignation.RECOVERY)
|
recovery_flow = self.executor.flow.related_flow(FlowDesignation.RECOVERY)
|
||||||
if recovery_flow:
|
if recovery_flow:
|
||||||
kwargs["recovery_url"] = reverse(
|
kwargs["recovery_url"] = reverse(
|
||||||
"passbook_flows:flow-executor",
|
"passbook_flows:flow-executor-shell",
|
||||||
kwargs={"flow_slug": recovery_flow.slug},
|
kwargs={"flow_slug": recovery_flow.slug},
|
||||||
)
|
)
|
||||||
kwargs["primary_action"] = _("Log in")
|
kwargs["primary_action"] = _("Log in")
|
||||||
|
|
|
@ -38,6 +38,7 @@ class PromptSerializer(ModelSerializer):
|
||||||
"type",
|
"type",
|
||||||
"required",
|
"required",
|
||||||
"placeholder",
|
"placeholder",
|
||||||
|
"order",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ class PromptAdminForm(forms.ModelForm):
|
||||||
"type",
|
"type",
|
||||||
"required",
|
"required",
|
||||||
"placeholder",
|
"placeholder",
|
||||||
|
"order",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"label": forms.TextInput(),
|
"label": forms.TextInput(),
|
||||||
|
@ -48,9 +49,12 @@ class PromptForm(forms.Form):
|
||||||
self.stage = stage
|
self.stage = stage
|
||||||
self.plan = plan
|
self.plan = plan
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
for field in self.stage.fields.all():
|
# list() is called so we only load the fields once
|
||||||
|
fields = list(self.stage.fields.all())
|
||||||
|
for field in fields:
|
||||||
field: Prompt
|
field: Prompt
|
||||||
self.fields[field.field_key] = field.field
|
self.fields[field.field_key] = field.field
|
||||||
|
self.field_order = sorted(fields, key=lambda x: x.order)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|
35
passbook/stages/prompt/migrations/0002_auto_20200528_2059.py
Normal file
35
passbook/stages/prompt/migrations/0002_auto_20200528_2059.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 3.0.6 on 2020-05-28 20:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_stages_prompt", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="prompt", name="order", field=models.IntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="prompt",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("text", "Text"),
|
||||||
|
("e-mail", "Email"),
|
||||||
|
("password", "Password"),
|
||||||
|
("number", "Number"),
|
||||||
|
("checkbox", "Checkbox"),
|
||||||
|
("data", "Date"),
|
||||||
|
("data-time", "Date Time"),
|
||||||
|
("separator", "Separator"),
|
||||||
|
("hidden", "Hidden"),
|
||||||
|
("static", "Static"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -16,7 +16,13 @@ class FieldTypes(models.TextChoices):
|
||||||
EMAIL = "e-mail"
|
EMAIL = "e-mail"
|
||||||
PASSWORD = "password" # noqa # nosec
|
PASSWORD = "password" # noqa # nosec
|
||||||
NUMBER = "number"
|
NUMBER = "number"
|
||||||
|
CHECKBOX = "checkbox"
|
||||||
|
DATE = "data"
|
||||||
|
DATE_TIME = "data-time"
|
||||||
|
|
||||||
|
SEPARATOR = "separator"
|
||||||
HIDDEN = "hidden"
|
HIDDEN = "hidden"
|
||||||
|
STATIC = "static"
|
||||||
|
|
||||||
|
|
||||||
class Prompt(models.Model):
|
class Prompt(models.Model):
|
||||||
|
@ -32,41 +38,37 @@ class Prompt(models.Model):
|
||||||
required = models.BooleanField(default=True)
|
required = models.BooleanField(default=True)
|
||||||
placeholder = models.TextField()
|
placeholder = models.TextField()
|
||||||
|
|
||||||
|
order = models.IntegerField(default=0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def field(self):
|
def field(self):
|
||||||
"""Return instantiated form input field"""
|
"""Return instantiated form input field"""
|
||||||
attrs = {"placeholder": _(self.placeholder)}
|
attrs = {"placeholder": _(self.placeholder)}
|
||||||
if self.type == FieldTypes.TEXT:
|
field_class = forms.CharField
|
||||||
return forms.CharField(
|
widget = forms.TextInput(attrs=attrs)
|
||||||
label=_(self.label),
|
kwargs = {
|
||||||
widget=forms.TextInput(attrs=attrs),
|
"label": _(self.label),
|
||||||
required=self.required,
|
"required": self.required,
|
||||||
)
|
}
|
||||||
if self.type == FieldTypes.EMAIL:
|
if self.type == FieldTypes.EMAIL:
|
||||||
return forms.EmailField(
|
field_class = forms.EmailField
|
||||||
label=_(self.label),
|
|
||||||
widget=forms.TextInput(attrs=attrs),
|
|
||||||
required=self.required,
|
|
||||||
)
|
|
||||||
if self.type == FieldTypes.PASSWORD:
|
if self.type == FieldTypes.PASSWORD:
|
||||||
return forms.CharField(
|
widget = forms.PasswordInput(attrs=attrs)
|
||||||
label=_(self.label),
|
|
||||||
widget=forms.PasswordInput(attrs=attrs),
|
|
||||||
required=self.required,
|
|
||||||
)
|
|
||||||
if self.type == FieldTypes.NUMBER:
|
if self.type == FieldTypes.NUMBER:
|
||||||
return forms.IntegerField(
|
field_class = forms.IntegerField
|
||||||
label=_(self.label),
|
widget = forms.NumberInput(attrs=attrs)
|
||||||
widget=forms.NumberInput(attrs=attrs),
|
|
||||||
required=self.required,
|
|
||||||
)
|
|
||||||
if self.type == FieldTypes.HIDDEN:
|
if self.type == FieldTypes.HIDDEN:
|
||||||
return forms.CharField(
|
widget = forms.HiddenInput(attrs=attrs)
|
||||||
widget=forms.HiddenInput(attrs=attrs),
|
kwargs["required"] = False
|
||||||
required=False,
|
kwargs["initial"] = self.placeholder
|
||||||
initial=self.placeholder,
|
if self.type == FieldTypes.CHECKBOX:
|
||||||
)
|
field_class = forms.CheckboxInput
|
||||||
raise ValueError("field_type is not valid, not one of FieldTypes.")
|
kwargs["required"] = False
|
||||||
|
|
||||||
|
# TODO: Implement static
|
||||||
|
# TODO: Implement separator
|
||||||
|
kwargs["widget"] = widget
|
||||||
|
return field_class(**kwargs)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.type not in FieldTypes:
|
if self.type not in FieldTypes:
|
||||||
|
|
|
@ -93,25 +93,6 @@ class TestPromptStage(TestCase):
|
||||||
|
|
||||||
FlowStageBinding.objects.create(flow=self.flow, stage=self.stage, order=2)
|
FlowStageBinding.objects.create(flow=self.flow, stage=self.stage, order=2)
|
||||||
|
|
||||||
def test_invalid_type(self):
|
|
||||||
"""Test that invalid form type raises an error"""
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
_ = Prompt.objects.create(
|
|
||||||
field_key="hidden_prompt",
|
|
||||||
type="invalid",
|
|
||||||
required=True,
|
|
||||||
placeholder="HIDDEN_PLACEHOLDER",
|
|
||||||
)
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
prompt = Prompt.objects.create(
|
|
||||||
field_key="hidden_prompt",
|
|
||||||
type=FieldTypes.HIDDEN,
|
|
||||||
required=True,
|
|
||||||
placeholder="HIDDEN_PLACEHOLDER",
|
|
||||||
)
|
|
||||||
with patch.object(prompt, "type", MagicMock(return_value="invalid")):
|
|
||||||
_ = prompt.field
|
|
||||||
|
|
||||||
def test_render(self):
|
def test_render(self):
|
||||||
"""Test render of form, check if all prompts are rendered correctly"""
|
"""Test render of form, check if all prompts are rendered correctly"""
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, stages=[self.stage])
|
||||||
|
|
|
@ -25,33 +25,30 @@ class UserWriteStageView(StageView):
|
||||||
LOGGER.debug(message)
|
LOGGER.debug(message)
|
||||||
return self.executor.stage_invalid()
|
return self.executor.stage_invalid()
|
||||||
data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
|
data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
|
||||||
if PLAN_CONTEXT_PENDING_USER in self.executor.plan.context:
|
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||||
user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User()
|
||||||
for key, value in data.items():
|
|
||||||
setter_name = f"set_{key}"
|
|
||||||
# Check if user has a setter for this key, like set_password
|
|
||||||
if hasattr(user, setter_name):
|
|
||||||
setter = getattr(user, setter_name)
|
|
||||||
if callable(setter):
|
|
||||||
setter(value)
|
|
||||||
# User has this key already
|
|
||||||
elif hasattr(user, key):
|
|
||||||
setattr(user, key, value)
|
|
||||||
# Otherwise we just save it as custom attribute
|
|
||||||
else:
|
|
||||||
user.attributes[key] = value
|
|
||||||
user.save()
|
|
||||||
LOGGER.debug(
|
|
||||||
"Updated existing user", user=user, flow_slug=self.executor.flow.slug,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
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[
|
self.executor.plan.context[
|
||||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
] = class_to_path(ModelBackend)
|
] = class_to_path(ModelBackend)
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Created new user", user=user, flow_slug=self.executor.flow.slug,
|
"Created new user", flow_slug=self.executor.flow.slug,
|
||||||
)
|
)
|
||||||
|
user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||||
|
for key, value in data.items():
|
||||||
|
setter_name = f"set_{key}"
|
||||||
|
# Check if user has a setter for this key, like set_password
|
||||||
|
if hasattr(user, setter_name):
|
||||||
|
setter = getattr(user, setter_name)
|
||||||
|
if callable(setter):
|
||||||
|
setter(value)
|
||||||
|
# User has this key already
|
||||||
|
elif hasattr(user, key):
|
||||||
|
setattr(user, key, value)
|
||||||
|
# Otherwise we just save it as custom attribute
|
||||||
|
else:
|
||||||
|
user.attributes[key] = value
|
||||||
|
user.save()
|
||||||
|
LOGGER.debug(
|
||||||
|
"Updated existing user", user=user, flow_slug=self.executor.flow.slug,
|
||||||
|
)
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|
10
swagger.yaml
10
swagger.yaml
|
@ -6028,7 +6028,12 @@ definitions:
|
||||||
- e-mail
|
- e-mail
|
||||||
- password
|
- password
|
||||||
- number
|
- number
|
||||||
|
- checkbox
|
||||||
|
- data
|
||||||
|
- data-time
|
||||||
|
- separator
|
||||||
- hidden
|
- hidden
|
||||||
|
- static
|
||||||
required:
|
required:
|
||||||
title: Required
|
title: Required
|
||||||
type: boolean
|
type: boolean
|
||||||
|
@ -6036,6 +6041,11 @@ definitions:
|
||||||
title: Placeholder
|
title: Placeholder
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
|
order:
|
||||||
|
title: Order
|
||||||
|
type: integer
|
||||||
|
maximum: 2147483647
|
||||||
|
minimum: -2147483648
|
||||||
PromptStage:
|
PromptStage:
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
|
|
Reference in a new issue