Merge branch 'master' into e2e

This commit is contained in:
Jens Langhammer 2020-05-29 00:23:47 +02:00
commit 467b95cf02
12 changed files with 111 additions and 76 deletions

View File

@ -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 %}

View File

@ -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"

View File

@ -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)

View File

@ -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}`);

View File

@ -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")

View File

@ -38,6 +38,7 @@ class PromptSerializer(ModelSerializer):
"type", "type",
"required", "required",
"placeholder", "placeholder",
"order",
] ]

View File

@ -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()

View 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,
),
),
]

View File

@ -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:

View File

@ -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])

View File

@ -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()

View File

@ -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