stages/prompt: Add initial_data prompt field and ability to select a default choice for choice fields (#5095)

* Added initial_value to model

* Added initial_value to admin panel

* Added initial_value support to flows; updated tests

* Updated default blueprints

* update docs

* Fix test

* Fix another test

* Fix yet another test

* Add placeholder migration

* Remove unused import
This commit is contained in:
sdimovv 2023-04-19 11:27:51 +01:00 committed by GitHub
parent 04cc7817ee
commit ee6edec1d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 418 additions and 138 deletions

View File

@ -59,6 +59,7 @@ class TestPasswordPolicyFlow(FlowTestCase):
"label": "PASSWORD_LABEL",
"order": 0,
"placeholder": "PASSWORD_PLACEHOLDER",
"initial_value": "",
"required": True,
"type": "password",
"sub_text": "",

View File

@ -57,10 +57,12 @@ class PromptSerializer(ModelSerializer):
"type",
"required",
"placeholder",
"initial_value",
"order",
"promptstage_set",
"sub_text",
"placeholder_expression",
"initial_value_expression",
]

View File

@ -0,0 +1,53 @@
# Generated by Django 4.1.7 on 2023-03-24 17:32
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_placeholder_expressions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.stages.prompt.models import CHOICE_FIELDS
db_alias = schema_editor.connection.alias
Prompt = apps.get_model("authentik_stages_prompt", "prompt")
for prompt in Prompt.objects.using(db_alias).all():
if not prompt.placeholder_expression or prompt.type in CHOICE_FIELDS:
continue
prompt.initial_value = prompt.placeholder
prompt.initial_value_expression = True
prompt.placeholder = ""
prompt.placeholder_expression = False
prompt.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_prompt", "0010_alter_prompt_placeholder_alter_prompt_type"),
]
operations = [
migrations.AddField(
model_name="prompt",
name="initial_value",
field=models.TextField(
blank=True,
help_text="Optionally pre-fill the input with an initial value. When creating a fixed choice field, enable interpreting as expression and return a list to return multiple default choices.",
),
),
migrations.AddField(
model_name="prompt",
name="initial_value_expression",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="prompt",
name="placeholder",
field=models.TextField(
blank=True,
help_text="Optionally provide a short hint that describes the expected input value. When creating a fixed choice field, enable interpreting as expression and return a list to return multiple choices.",
),
),
migrations.RunPython(code=migrate_placeholder_expressions),
]

View File

@ -29,6 +29,8 @@ from authentik.flows.models import Stage
from authentik.lib.models import SerializerModel
from authentik.policies.models import Policy
CHOICES_CONTEXT_SUFFIX = "__choices"
LOGGER = get_logger()
@ -119,15 +121,25 @@ class Prompt(SerializerModel):
placeholder = models.TextField(
blank=True,
help_text=_(
"When creating a Radio Button Group or Dropdown, enable interpreting as "
"Optionally provide a short hint that describes the expected input value. "
"When creating a fixed choice field, enable interpreting as "
"expression and return a list to return multiple choices."
),
)
initial_value = models.TextField(
blank=True,
help_text=_(
"Optionally pre-fill the input with an initial value. "
"When creating a fixed choice field, enable interpreting as "
"expression and return a list to return multiple default choices."
),
)
sub_text = models.TextField(blank=True, default="")
order = models.IntegerField(default=0)
placeholder_expression = models.BooleanField(default=False)
initial_value_expression = models.BooleanField(default=False)
@property
def serializer(self) -> Type[BaseSerializer]:
@ -148,8 +160,8 @@ class Prompt(SerializerModel):
raw_choices = self.placeholder
if self.field_key in prompt_context:
raw_choices = prompt_context[self.field_key]
if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context:
raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX]
elif self.placeholder_expression:
evaluator = PropertyMappingEvaluator(
self, user, request, prompt_context=prompt_context, dry_run=dry_run
@ -184,16 +196,9 @@ class Prompt(SerializerModel):
) -> str:
"""Get fully interpolated placeholder"""
if self.type in CHOICE_FIELDS:
# Make sure to return a valid choice as placeholder
choices = self.get_choices(prompt_context, user, request, dry_run=dry_run)
if not choices:
# Choice fields use the placeholder to define all valid choices.
# Therefore their actual placeholder is always blank
return ""
return choices[0]
if self.field_key in prompt_context:
# We don't want to parse this as an expression since a user will
# be able to control the input
return prompt_context[self.field_key]
if self.placeholder_expression:
evaluator = PropertyMappingEvaluator(
@ -211,6 +216,47 @@ class Prompt(SerializerModel):
raise wrapped from exc
return self.placeholder
def get_initial_value(
self,
prompt_context: dict,
user: User,
request: HttpRequest,
dry_run: Optional[bool] = False,
) -> str:
"""Get fully interpolated initial value"""
if self.field_key in prompt_context:
# We don't want to parse this as an expression since a user will
# be able to control the input
value = prompt_context[self.field_key]
elif self.initial_value_expression:
evaluator = PropertyMappingEvaluator(
self, user, request, prompt_context=prompt_context, dry_run=dry_run
)
try:
value = evaluator.evaluate(self.initial_value)
except Exception as exc: # pylint:disable=broad-except
wrapped = PropertyMappingExpressionException(str(exc))
LOGGER.warning(
"failed to evaluate prompt initial value",
exc=wrapped,
)
if dry_run:
raise wrapped from exc
value = self.initial_value
else:
value = self.initial_value
if self.type in CHOICE_FIELDS:
# Ensure returned value is a valid choice
choices = self.get_choices(prompt_context, user, request)
if not choices:
return ""
if value not in choices:
return choices[0]
return value
def field(self, default: Optional[Any], choices: Optional[list[Any]] = None) -> CharField:
"""Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS."""
field_class = CharField

View File

@ -38,6 +38,7 @@ class StagePromptSerializer(PassiveSerializer):
type = ChoiceField(choices=FieldTypes.choices)
required = BooleanField()
placeholder = CharField(allow_blank=True)
initial_value = CharField(allow_blank=True)
order = IntegerField()
sub_text = CharField(allow_blank=True)
choices = ListField(child=CharField(allow_blank=True), allow_empty=True, allow_null=True)
@ -76,7 +77,7 @@ class PromptChallengeResponse(ChallengeResponse):
choices = field.get_choices(
plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request
)
current = field.get_placeholder(
current = field.get_initial_value(
plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request
)
self.fields[field.field_key] = field.field(current, choices)
@ -197,8 +198,9 @@ class PromptStageView(ChallengeStageView):
serializers = []
for field in fields:
data = StagePromptSerializer(field).data
# Ensure all choices and placeholders are str, as otherwise further in
# we can fail serializer validation if we return some types such as bool
# Ensure all choices, placeholders and initial values are str, as
# otherwise further in we can fail serializer validation if we return
# some types such as bool
choices = field.get_choices(context, self.get_pending_user(), self.request, dry_run)
if choices:
data["choices"] = [str(choice) for choice in choices]
@ -207,6 +209,9 @@ class PromptStageView(ChallengeStageView):
data["placeholder"] = str(
field.get_placeholder(context, self.get_pending_user(), self.request, dry_run)
)
data["initial_value"] = str(
field.get_initial_value(context, self.get_pending_user(), self.request, dry_run)
)
serializers.append(data)
return serializers

View File

@ -22,6 +22,7 @@ from authentik.stages.prompt.stage import (
)
# pylint: disable=too-many-public-methods
class TestPromptStage(FlowTestCase):
"""Prompt tests"""
@ -37,6 +38,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.USERNAME,
required=True,
placeholder="USERNAME_PLACEHOLDER",
initial_value="akuser",
)
text_prompt = Prompt.objects.create(
name=generate_id(),
@ -45,6 +47,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.TEXT,
required=True,
placeholder="TEXT_PLACEHOLDER",
initial_value="some text",
)
text_area_prompt = Prompt.objects.create(
name=generate_id(),
@ -53,6 +56,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.TEXT_AREA,
required=True,
placeholder="TEXT_AREA_PLACEHOLDER",
initial_value="some text",
)
email_prompt = Prompt.objects.create(
name=generate_id(),
@ -61,6 +65,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.EMAIL,
required=True,
placeholder="EMAIL_PLACEHOLDER",
initial_value="email@example.com",
)
password_prompt = Prompt.objects.create(
name=generate_id(),
@ -69,6 +74,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.PASSWORD,
required=True,
placeholder="PASSWORD_PLACEHOLDER",
initial_value="supersecurepassword",
)
password2_prompt = Prompt.objects.create(
name=generate_id(),
@ -77,6 +83,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.PASSWORD,
required=True,
placeholder="PASSWORD_PLACEHOLDER",
initial_value="supersecurepassword",
)
number_prompt = Prompt.objects.create(
name=generate_id(),
@ -85,6 +92,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.NUMBER,
required=True,
placeholder="NUMBER_PLACEHOLDER",
initial_value="42",
)
hidden_prompt = Prompt.objects.create(
name=generate_id(),
@ -92,6 +100,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.HIDDEN,
required=True,
placeholder="HIDDEN_PLACEHOLDER",
initial_value="something idk",
)
static_prompt = Prompt.objects.create(
name=generate_id(),
@ -99,6 +108,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.STATIC,
required=True,
placeholder="static",
initial_value="something idk",
)
radio_button_group = Prompt.objects.create(
name=generate_id(),
@ -106,6 +116,7 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.RADIO_BUTTON_GROUP,
required=True,
placeholder="test",
initial_value="test",
)
dropdown = Prompt.objects.create(
name=generate_id(),
@ -137,9 +148,9 @@ class TestPromptStage(FlowTestCase):
password_prompt.field_key: "test",
password2_prompt.field_key: "test",
number_prompt.field_key: 3,
hidden_prompt.field_key: hidden_prompt.placeholder,
static_prompt.field_key: static_prompt.placeholder,
radio_button_group.field_key: radio_button_group.placeholder,
hidden_prompt.field_key: hidden_prompt.initial_value,
static_prompt.field_key: static_prompt.initial_value,
radio_button_group.field_key: radio_button_group.initial_value,
dropdown.field_key: "",
}
@ -335,106 +346,176 @@ class TestPromptStage(FlowTestCase):
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
context["text_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")),
context["text_prompt_expression"],
def test_prompt_placeholder_does_not_take_value_from_context(self):
"""Test placeholder does not automatically take value from context"""
context = {
"foo": generate_id(),
}
prompt: Prompt = Prompt(
field_key="text_prompt_expression",
label="TEXT_LABEL",
type=FieldTypes.TEXT,
placeholder="return prompt_context['foo']",
placeholder_expression=True,
)
self.assertNotEqual(
context["text_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
def test_choice_prompts_placeholders(self):
"""Test placeholders and expression of choice fields"""
context = {"foo": generate_id()}
def test_prompt_initial_value(self):
"""Test initial_value and expression"""
context = {
"foo": generate_id(),
}
prompt: Prompt = Prompt(
field_key="text_prompt_expression",
label="TEXT_LABEL",
type=FieldTypes.TEXT,
initial_value="return prompt_context['foo']",
initial_value_expression=True,
)
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"]
)
context["text_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")),
context["text_prompt_expression"],
)
self.assertNotEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"]
)
def test_choice_prompts_placeholder_and_initial_value_no_choices(self):
"""Test placeholder and initial value of choice fields with 0 choices"""
context = {}
# No choices - unusable (in the sense it creates an unsubmittable form)
# but valid behaviour
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return []",
placeholder_expression=True,
initial_value="Invalid choice",
initial_value_expression=False,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(prompt.get_initial_value(context, self.user, self.factory.get("/")), "")
self.assertEqual(prompt.get_choices(context, self.user, self.factory.get("/")), tuple())
context["fixed_choice_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")),
context["fixed_choice_prompt_expression"],
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["fixed_choice_prompt_expression"],),
)
self.assertNotEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertNotEqual(prompt.get_choices(context, self.user, self.factory.get("/")), tuple())
del context["fixed_choice_prompt_expression"]
def test_choice_prompts_placeholder_and_initial_value_single_choice(self):
"""Test placeholder and initial value of choice fields with 1 choice"""
context = {"foo": generate_id()}
# Single choice
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return prompt_context['foo']",
placeholder_expression=True,
)
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
context["fixed_choice_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")),
context["fixed_choice_prompt_expression"],
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["fixed_choice_prompt_expression"],),
)
self.assertNotEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertNotEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
del context["fixed_choice_prompt_expression"]
# Multi choice
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.DROPDOWN,
placeholder="return [prompt_context['foo'], True, 'text']",
placeholder=context["foo"],
placeholder_expression=False,
initial_value=context["foo"],
initial_value_expression=False,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.DROPDOWN,
placeholder="return [prompt_context['foo']]",
placeholder_expression=True,
initial_value="return prompt_context['foo']",
initial_value_expression=True,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
def test_choice_prompts_placeholder_and_initial_value_multiple_choices(self):
"""Test placeholder and initial value of choice fields with multiple choices"""
context = {}
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return ['test', True, 42]",
placeholder_expression=True,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
prompt.get_initial_value(context, self.user, self.factory.get("/")), "test"
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), ("test", True, 42)
)
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return ['test', True, 42]",
placeholder_expression=True,
initial_value="return True",
initial_value_expression=True,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(prompt.get_initial_value(context, self.user, self.factory.get("/")), True)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), ("test", True, 42)
)
def test_choice_prompts_placeholder_and_initial_value_from_context(self):
"""Test placeholder and initial value of choice fields with values from context"""
rand_value = generate_id()
context = {
"fixed_choice_prompt_expression": rand_value,
"fixed_choice_prompt_expression__choices": ["test", 42, rand_value],
}
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), rand_value
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), ("test", 42, rand_value)
)
def test_initial_value_not_valid_choice(self):
"""Test initial_value not a valid choice"""
context = {}
prompt: Prompt = Prompt(
field_key="choice_prompt",
label="TEXT_LABEL",
type=FieldTypes.DROPDOWN,
placeholder="choice",
initial_value="another_choice",
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["foo"], True, "text"),
)
context["fixed_choice_prompt_expression"] = tuple(["text", generate_id(), 2])
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")),
"text",
("choice",),
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
context["fixed_choice_prompt_expression"],
)
self.assertNotEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertNotEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["foo"], True, "text"),
prompt.get_initial_value(context, self.user, self.factory.get("/")),
"choice",
)
def test_choices_are_none_for_non_choice_fields(self):
@ -505,6 +586,8 @@ class TestPromptStage(FlowTestCase):
"type": FieldTypes.TEXT,
"placeholder": 'return "Hello world"',
"placeholder_expression": True,
"initial_value": 'return "Hello Hello world"',
"initial_value_expression": True,
"sub_text": "test",
"order": 123,
},
@ -522,6 +605,7 @@ class TestPromptStage(FlowTestCase):
"type": "text",
"required": True,
"placeholder": "Hello world",
"initial_value": "Hello Hello world",
"order": 123,
"sub_text": "test",
"choices": None,

View File

@ -13,12 +13,14 @@ entries:
id: flow
- attrs:
order: 200
placeholder: |
placeholder: Username
placeholder_expression: false
initial_value: |
try:
return user.username
except:
return ''
placeholder_expression: true
initial_value_expression: true
required: true
type: text
field_key: username
@ -29,12 +31,14 @@ entries:
model: authentik_stages_prompt.prompt
- attrs:
order: 201
placeholder: |
placeholder: Name
placeholder_expression: false
initial_value: |
try:
return user.name
except:
return ''
placeholder_expression: true
initial_value_expression: true
required: true
type: text
field_key: name
@ -45,12 +49,14 @@ entries:
model: authentik_stages_prompt.prompt
- attrs:
order: 202
placeholder: |
placeholder: Email
placeholder_expression: false
initial_value: |
try:
return user.email
except:
return ''
placeholder_expression: true
initial_value_expression: true
required: true
type: email
field_key: email
@ -61,12 +67,14 @@ entries:
model: authentik_stages_prompt.prompt
- attrs:
order: 203
placeholder: |
placeholder: Locale
placeholder_expression: false
initial_value: |
try:
return user.attributes.get("settings", {}).get("locale", "")
except:
return ''
placeholder_expression: true
initial_value_expression: true
required: true
type: ak-locale
field_key: attributes.settings.locale

View File

@ -36862,8 +36862,14 @@ components:
type: boolean
placeholder:
type: string
description: When creating a Radio Button Group or Dropdown, enable interpreting
as expression and return a list to return multiple choices.
description: Optionally provide a short hint that describes the expected
input value. When creating a fixed choice field, enable interpreting as
expression and return a list to return multiple choices.
initial_value:
type: string
description: Optionally pre-fill the input with an initial value. When creating
a fixed choice field, enable interpreting as expression and return a list
to return multiple default choices.
order:
type: integer
maximum: 2147483647
@ -36876,6 +36882,8 @@ components:
type: string
placeholder_expression:
type: boolean
initial_value_expression:
type: boolean
PatchedPromptStageRequest:
type: object
description: PromptStage Serializer
@ -38034,8 +38042,14 @@ components:
type: boolean
placeholder:
type: string
description: When creating a Radio Button Group or Dropdown, enable interpreting
as expression and return a list to return multiple choices.
description: Optionally provide a short hint that describes the expected
input value. When creating a fixed choice field, enable interpreting as
expression and return a list to return multiple choices.
initial_value:
type: string
description: Optionally pre-fill the input with an initial value. When creating
a fixed choice field, enable interpreting as expression and return a list
to return multiple default choices.
order:
type: integer
maximum: 2147483647
@ -38048,6 +38062,8 @@ components:
type: string
placeholder_expression:
type: boolean
initial_value_expression:
type: boolean
required:
- field_key
- label
@ -38109,8 +38125,14 @@ components:
type: boolean
placeholder:
type: string
description: When creating a Radio Button Group or Dropdown, enable interpreting
as expression and return a list to return multiple choices.
description: Optionally provide a short hint that describes the expected
input value. When creating a fixed choice field, enable interpreting as
expression and return a list to return multiple choices.
initial_value:
type: string
description: Optionally pre-fill the input with an initial value. When creating
a fixed choice field, enable interpreting as expression and return a list
to return multiple default choices.
order:
type: integer
maximum: 2147483647
@ -38123,6 +38145,8 @@ components:
type: string
placeholder_expression:
type: boolean
initial_value_expression:
type: boolean
required:
- field_key
- label
@ -40267,6 +40291,8 @@ components:
type: boolean
placeholder:
type: string
initial_value:
type: string
order:
type: integer
sub_text:
@ -40279,6 +40305,7 @@ components:
required:
- choices
- field_key
- initial_value
- label
- order
- placeholder

View File

@ -372,8 +372,8 @@ export class PromptForm extends ModelForm<Prompt, string> {
>
</label>
<p class="pf-c-form__helper-text">
${t`When checked, the placeholder will be evaluated in the same way environment as a property mapping.
If the evaluation failed, the placeholder itself is returned.`}
${t`When checked, the placeholder will be evaluated in the same way a property mapping is.
If the evaluation fails, the placeholder itself is returned.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Placeholder`} name="placeholder">
@ -386,11 +386,41 @@ export class PromptForm extends ModelForm<Prompt, string> {
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${t`Optionally pre-fill the input value.
When creating a "Radio Button Group" or "Dropdown", enable interpreting as
${t`Optionally provide a short hint that describes the expected input value.
When creating a fixed choice field, enable interpreting as
expression and return a list to return multiple choices.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="initialValueExpression">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.initialValueExpression, false)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${t`Interpret initial value as expression`}</span
>
</label>
<p class="pf-c-form__helper-text">
${t`When checked, the initial value will be evaluated in the same way a property mapping is.
If the evaluation fails, the initial value itself is returned.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Initial value`} name="initialValue">
<ak-codemirror mode="python" value="${ifDefined(this.instance?.initialValue)}">
</ak-codemirror>
<p class="pf-c-form__helper-text">
${t`Optionally pre-fill the input with an initial value.
When creating a fixed choice field, enable interpreting as
expression and return a list to return multiple default choices.`}}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Help text`} name="subText">
<ak-codemirror
mode="htmlmixed"

View File

@ -48,7 +48,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
];
}
renderPromptInner(prompt: StagePrompt, placeholderAsValue: boolean): string {
renderPromptInner(prompt: StagePrompt): string {
switch (prompt.type) {
case PromptTypeEnum.Text:
return `<input
@ -58,7 +58,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
autocomplete="off"
class="pf-c-form-control"
?required=${prompt.required}
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
value="${prompt.initialValue}">`;
case PromptTypeEnum.TextArea:
return `<textarea
type="text"
@ -67,21 +67,23 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
autocomplete="off"
class="pf-c-form-control"
?required=${prompt.required}
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
value="${prompt.initialValue}"">`;
case PromptTypeEnum.TextReadOnly:
return `<input
type="text"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
readonly
value="${prompt.placeholder}">`;
value="${prompt.initialValue}">`;
case PromptTypeEnum.TextAreaReadOnly:
return `<textarea
type="text"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
readonly
value="${prompt.placeholder}">`;
value="${prompt.initialValue}">`;
case PromptTypeEnum.Username:
return `<input
type="text"
@ -90,7 +92,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
autocomplete="username"
class="pf-c-form-control"
?required=${prompt.required}
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
value="${prompt.initialValue}">`;
case PromptTypeEnum.Email:
return `<input
type="email"
@ -98,7 +100,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
value="${prompt.initialValue}">`;
case PromptTypeEnum.Password:
return `<input
type="password"
@ -113,46 +115,50 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
?required=${prompt.required}
value="${prompt.initialValue}">`;
case PromptTypeEnum.Date:
return `<input
type="date"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
?required=${prompt.required}
value="${prompt.initialValue}">`;
case PromptTypeEnum.DateTime:
return `<input
type="datetime"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
?required=${prompt.required}
value="${prompt.initialValue}">`;
case PromptTypeEnum.File:
return `<input
type="file"
name="${prompt.fieldKey}"
placeholder="${prompt.placeholder}"
class="pf-c-form-control"
?required=${prompt.required}>`;
?required=${prompt.required}
value="${prompt.initialValue}">`;
case PromptTypeEnum.Separator:
return `<ak-divider>${prompt.placeholder}</ak-divider>`;
case PromptTypeEnum.Hidden:
return `<input
type="hidden"
name="${prompt.fieldKey}"
value="${prompt.placeholder}"
value="${prompt.initialValue}"
class="pf-c-form-control"
?required=${prompt.required}>`;
case PromptTypeEnum.Static:
return `<p>${prompt.placeholder}</p>`;
return `<p>${prompt.initialValue}</p>`;
case PromptTypeEnum.Dropdown:
return `<select class="pf-c-form-control" name="${prompt.fieldKey}">
${prompt.choices
?.map((choice) => {
return `<option
value="${choice}"
?selected=${prompt.placeholder === choice}
?selected=${prompt.initialValue === choice}
>
${choice}
</option>`;
@ -168,7 +174,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
type="radio"
class="pf-c-check__input"
name="${prompt.fieldKey}"
checked="${prompt.placeholder === choice}"
checked="${prompt.initialValue === choice}"
required="${prompt.required}"
value="${choice}"
/>
@ -180,7 +186,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
);
case PromptTypeEnum.AkLocale:
return `<select class="pf-c-form-control" name="${prompt.fieldKey}">
<option value="" ${prompt.placeholder === "" ? "selected" : ""}>
<option value="" ${prompt.initialValue === "" ? "selected" : ""}>
${t`Auto-detect (based on your browser)`}
</option>
${LOCALES.filter((locale) => {
@ -195,7 +201,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
.map((locale) => {
return `<option
value=${locale.code}
${prompt.placeholder === locale.code ? "selected" : ""}
${prompt.initialValue === locale.code ? "selected" : ""}
>
${locale.code.toUpperCase()} - ${locale.label}
</option>`;
@ -234,7 +240,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
type="checkbox"
class="pf-c-check__input"
name="${prompt.fieldKey}"
?checked=${prompt.placeholder !== ""}
?checked=${prompt.initialValue !== ""}
?required=${prompt.required}
/>
<label class="pf-c-check__label">${prompt.label}</label>
@ -251,11 +257,10 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
>
${unsafeHTML(this.renderPromptInner(prompt, false))}
${this.renderPromptHelpText(prompt)}
${unsafeHTML(this.renderPromptInner(prompt))} ${this.renderPromptHelpText(prompt)}
</ak-form-element>`;
}
return html` ${unsafeHTML(this.renderPromptInner(prompt, false))}
return html` ${unsafeHTML(this.renderPromptInner(prompt))}
${this.renderPromptHelpText(prompt)}`;
}

View File

@ -17,7 +17,7 @@ export class UserSettingsPromptStage extends PromptStage {
return super.styles.concat(PFCheck);
}
renderPromptInner(prompt: StagePrompt, placeholderAsValue: boolean): string {
renderPromptInner(prompt: StagePrompt): string {
switch (prompt.type) {
// Checkbox requires slightly different rendering here due to the use of horizontal form elements
case PromptTypeEnum.Checkbox:
@ -25,12 +25,12 @@ export class UserSettingsPromptStage extends PromptStage {
type="checkbox"
class="pf-c-check__input"
name="${prompt.fieldKey}"
?checked=${prompt.placeholder !== ""}
?checked=${prompt.initialValue !== ""}
?required=${prompt.required}
style="vertical-align: bottom"
/>`;
default:
return super.renderPromptInner(prompt, placeholderAsValue);
return super.renderPromptInner(prompt);
}
}
@ -47,13 +47,13 @@ export class UserSettingsPromptStage extends PromptStage {
return error.string;
})}
>
${unsafeHTML(this.renderPromptInner(prompt, true))}
${unsafeHTML(this.renderPromptInner(prompt))}
${this.renderPromptHelpText(prompt)}
</ak-form-element-horizontal>
`;
}
return html`
${unsafeHTML(this.renderPromptInner(prompt, true))} ${this.renderPromptHelpText(prompt)}
${unsafeHTML(this.renderPromptInner(prompt))} ${this.renderPromptHelpText(prompt)}
`;
}

View File

@ -37,7 +37,7 @@ Some types have special behaviors:
- _Username_: Input is validated against other usernames to ensure a unique value is provided.
- _Password_: All prompts with the type password within the same stage are compared and must be equal. If they are not equal, an error is shown
- _Hidden_ and _Static_: Their placeholder values are defaults and are not user-changeable.
- _Hidden_ and _Static_: Their initial values are defaults and are not user-changeable.
- _Radio Button Group_ and _Dropdown_: Only allow the user to select one of a set of predefined values.
A prompt has the following attributes:
@ -60,15 +60,34 @@ A flag which decides whether or not this field is required.
### `placeholder`
A field placeholder, shown within the input field. This field is also used by the `hidden` type as the actual value.
A field placeholder, shown within the input field.
By default, the placeholder is interpreted as-is. If you enable _Interpret placeholder as expression_, the placeholder
will be evaluated as a python expression. This happens in the same environment as [_Property mappings_](../../../property-mappings/expression).
In the case of `Radio Button Group` and `Dropdown` prompts, this field defines all possible values. When interpreted as-is, only one value will be allowed (the placeholder string). When interpreted as expression, a list of values can be returned to define multiple choices. For example, `return ["first option", 42, "another option"]` defines 3 possible values.
In the case of `Radio Button Group` and `Dropdown` prompts, this field defines all possible values (choices). When interpreted as-is, only one value will be allowed (the placeholder string). When interpreted as expression, a list of values can be returned to define multiple choices. For example, `return ["first option", 42, "another option"]` defines 3 possible values.
You can access both the HTTP request and the user as with a mapping. Additionally, you can access `prompt_context`, which is a dictionary of the current state of the prompt stage's data.
For `Radio Button Group` and `Dropdown` prompts, if a key with the same name as the prompt's `field_key` and a suffix of `__choices` (`<field_key>__choices`) is present in the `prompt_context` dictionary, its value will be returned directly, even if _Interpret placeholder as expression_ is enabled.
### `initial_value`
The prompt's initial value. It can also be left empty, in which case the field will not have a pre-filled value.
With the `hidden` prompt, the initial value will also be the actual value, because the field is hidden to the user.
By default, the initial value is interpreted as-is. If you enable _Interpret initial value as expression_, the initial value
will be evaluated as a python expression. This happens in the same environment as [_Property mappings_](../../../property-mappings/expression).
In the case of `Radio Button Group` and `Dropdown` prompts, this field defines the default choice. When interpreted as-is, the default choice will be the initial value string. When interpreted as expression, the default choice will be the returned value. For example, `return 42` defines `42` as the default choice.
:::note
The default choice defined for any fixed choice field **must** be one of the valid choices specified in the prompt's placeholder.
:::
You can access both the HTTP request and the user as with a mapping. Additionally, you can access `prompt_context`, which is a dictionary of the current state of the prompt stage's data. If a key with the same name as the prompt's `field_key` is present in the `prompt_context` dictionary, its value will be returned directly, even if _Interpret initial value as expression_ is enabled.
### `order`
The numerical index of the prompt. This applies to all stages which this prompt is a part of.