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:
parent
04cc7817ee
commit
ee6edec1d8
|
@ -59,6 +59,7 @@ class TestPasswordPolicyFlow(FlowTestCase):
|
|||
"label": "PASSWORD_LABEL",
|
||||
"order": 0,
|
||||
"placeholder": "PASSWORD_PLACEHOLDER",
|
||||
"initial_value": "",
|
||||
"required": True,
|
||||
"type": "password",
|
||||
"sub_text": "",
|
||||
|
|
|
@ -57,10 +57,12 @@ class PromptSerializer(ModelSerializer):
|
|||
"type",
|
||||
"required",
|
||||
"placeholder",
|
||||
"initial_value",
|
||||
"order",
|
||||
"promptstage_set",
|
||||
"sub_text",
|
||||
"placeholder_expression",
|
||||
"initial_value_expression",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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),
|
||||
]
|
|
@ -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:
|
||||
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]
|
||||
# Choice fields use the placeholder to define all valid choices.
|
||||
# Therefore their actual placeholder is always blank
|
||||
return ""
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
39
schema.yml
39
schema.yml
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)}
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Reference in New Issue