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",
|
"label": "PASSWORD_LABEL",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"placeholder": "PASSWORD_PLACEHOLDER",
|
"placeholder": "PASSWORD_PLACEHOLDER",
|
||||||
|
"initial_value": "",
|
||||||
"required": True,
|
"required": True,
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"sub_text": "",
|
"sub_text": "",
|
||||||
|
|
|
@ -57,10 +57,12 @@ class PromptSerializer(ModelSerializer):
|
||||||
"type",
|
"type",
|
||||||
"required",
|
"required",
|
||||||
"placeholder",
|
"placeholder",
|
||||||
|
"initial_value",
|
||||||
"order",
|
"order",
|
||||||
"promptstage_set",
|
"promptstage_set",
|
||||||
"sub_text",
|
"sub_text",
|
||||||
"placeholder_expression",
|
"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.lib.models import SerializerModel
|
||||||
from authentik.policies.models import Policy
|
from authentik.policies.models import Policy
|
||||||
|
|
||||||
|
CHOICES_CONTEXT_SUFFIX = "__choices"
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
@ -119,15 +121,25 @@ class Prompt(SerializerModel):
|
||||||
placeholder = models.TextField(
|
placeholder = models.TextField(
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_(
|
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."
|
"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="")
|
sub_text = models.TextField(blank=True, default="")
|
||||||
|
|
||||||
order = models.IntegerField(default=0)
|
order = models.IntegerField(default=0)
|
||||||
|
|
||||||
placeholder_expression = models.BooleanField(default=False)
|
placeholder_expression = models.BooleanField(default=False)
|
||||||
|
initial_value_expression = models.BooleanField(default=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Type[BaseSerializer]:
|
def serializer(self) -> Type[BaseSerializer]:
|
||||||
|
@ -148,8 +160,8 @@ class Prompt(SerializerModel):
|
||||||
|
|
||||||
raw_choices = self.placeholder
|
raw_choices = self.placeholder
|
||||||
|
|
||||||
if self.field_key in prompt_context:
|
if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context:
|
||||||
raw_choices = prompt_context[self.field_key]
|
raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX]
|
||||||
elif self.placeholder_expression:
|
elif self.placeholder_expression:
|
||||||
evaluator = PropertyMappingEvaluator(
|
evaluator = PropertyMappingEvaluator(
|
||||||
self, user, request, prompt_context=prompt_context, dry_run=dry_run
|
self, user, request, prompt_context=prompt_context, dry_run=dry_run
|
||||||
|
@ -184,16 +196,9 @@ class Prompt(SerializerModel):
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Get fully interpolated placeholder"""
|
"""Get fully interpolated placeholder"""
|
||||||
if self.type in CHOICE_FIELDS:
|
if self.type in CHOICE_FIELDS:
|
||||||
# Make sure to return a valid choice as placeholder
|
# Choice fields use the placeholder to define all valid choices.
|
||||||
choices = self.get_choices(prompt_context, user, request, dry_run=dry_run)
|
# Therefore their actual placeholder is always blank
|
||||||
if not choices:
|
return ""
|
||||||
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:
|
if self.placeholder_expression:
|
||||||
evaluator = PropertyMappingEvaluator(
|
evaluator = PropertyMappingEvaluator(
|
||||||
|
@ -211,6 +216,47 @@ class Prompt(SerializerModel):
|
||||||
raise wrapped from exc
|
raise wrapped from exc
|
||||||
return self.placeholder
|
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:
|
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."""
|
"""Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS."""
|
||||||
field_class = CharField
|
field_class = CharField
|
||||||
|
|
|
@ -38,6 +38,7 @@ class StagePromptSerializer(PassiveSerializer):
|
||||||
type = ChoiceField(choices=FieldTypes.choices)
|
type = ChoiceField(choices=FieldTypes.choices)
|
||||||
required = BooleanField()
|
required = BooleanField()
|
||||||
placeholder = CharField(allow_blank=True)
|
placeholder = CharField(allow_blank=True)
|
||||||
|
initial_value = CharField(allow_blank=True)
|
||||||
order = IntegerField()
|
order = IntegerField()
|
||||||
sub_text = CharField(allow_blank=True)
|
sub_text = CharField(allow_blank=True)
|
||||||
choices = ListField(child=CharField(allow_blank=True), allow_empty=True, allow_null=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(
|
choices = field.get_choices(
|
||||||
plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request
|
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
|
plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request
|
||||||
)
|
)
|
||||||
self.fields[field.field_key] = field.field(current, choices)
|
self.fields[field.field_key] = field.field(current, choices)
|
||||||
|
@ -197,8 +198,9 @@ class PromptStageView(ChallengeStageView):
|
||||||
serializers = []
|
serializers = []
|
||||||
for field in fields:
|
for field in fields:
|
||||||
data = StagePromptSerializer(field).data
|
data = StagePromptSerializer(field).data
|
||||||
# Ensure all choices and placeholders are str, as otherwise further in
|
# Ensure all choices, placeholders and initial values are str, as
|
||||||
# we can fail serializer validation if we return some types such as bool
|
# 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)
|
choices = field.get_choices(context, self.get_pending_user(), self.request, dry_run)
|
||||||
if choices:
|
if choices:
|
||||||
data["choices"] = [str(choice) for choice in choices]
|
data["choices"] = [str(choice) for choice in choices]
|
||||||
|
@ -207,6 +209,9 @@ class PromptStageView(ChallengeStageView):
|
||||||
data["placeholder"] = str(
|
data["placeholder"] = str(
|
||||||
field.get_placeholder(context, self.get_pending_user(), self.request, dry_run)
|
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)
|
serializers.append(data)
|
||||||
return serializers
|
return serializers
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ from authentik.stages.prompt.stage import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
class TestPromptStage(FlowTestCase):
|
class TestPromptStage(FlowTestCase):
|
||||||
"""Prompt tests"""
|
"""Prompt tests"""
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.USERNAME,
|
type=FieldTypes.USERNAME,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="USERNAME_PLACEHOLDER",
|
placeholder="USERNAME_PLACEHOLDER",
|
||||||
|
initial_value="akuser",
|
||||||
)
|
)
|
||||||
text_prompt = Prompt.objects.create(
|
text_prompt = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -45,6 +47,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.TEXT,
|
type=FieldTypes.TEXT,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="TEXT_PLACEHOLDER",
|
placeholder="TEXT_PLACEHOLDER",
|
||||||
|
initial_value="some text",
|
||||||
)
|
)
|
||||||
text_area_prompt = Prompt.objects.create(
|
text_area_prompt = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -53,6 +56,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.TEXT_AREA,
|
type=FieldTypes.TEXT_AREA,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="TEXT_AREA_PLACEHOLDER",
|
placeholder="TEXT_AREA_PLACEHOLDER",
|
||||||
|
initial_value="some text",
|
||||||
)
|
)
|
||||||
email_prompt = Prompt.objects.create(
|
email_prompt = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -61,6 +65,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.EMAIL,
|
type=FieldTypes.EMAIL,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="EMAIL_PLACEHOLDER",
|
placeholder="EMAIL_PLACEHOLDER",
|
||||||
|
initial_value="email@example.com",
|
||||||
)
|
)
|
||||||
password_prompt = Prompt.objects.create(
|
password_prompt = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -69,6 +74,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.PASSWORD,
|
type=FieldTypes.PASSWORD,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="PASSWORD_PLACEHOLDER",
|
placeholder="PASSWORD_PLACEHOLDER",
|
||||||
|
initial_value="supersecurepassword",
|
||||||
)
|
)
|
||||||
password2_prompt = Prompt.objects.create(
|
password2_prompt = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -77,6 +83,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.PASSWORD,
|
type=FieldTypes.PASSWORD,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="PASSWORD_PLACEHOLDER",
|
placeholder="PASSWORD_PLACEHOLDER",
|
||||||
|
initial_value="supersecurepassword",
|
||||||
)
|
)
|
||||||
number_prompt = Prompt.objects.create(
|
number_prompt = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -85,6 +92,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.NUMBER,
|
type=FieldTypes.NUMBER,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="NUMBER_PLACEHOLDER",
|
placeholder="NUMBER_PLACEHOLDER",
|
||||||
|
initial_value="42",
|
||||||
)
|
)
|
||||||
hidden_prompt = Prompt.objects.create(
|
hidden_prompt = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -92,6 +100,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.HIDDEN,
|
type=FieldTypes.HIDDEN,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="HIDDEN_PLACEHOLDER",
|
placeholder="HIDDEN_PLACEHOLDER",
|
||||||
|
initial_value="something idk",
|
||||||
)
|
)
|
||||||
static_prompt = Prompt.objects.create(
|
static_prompt = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -99,6 +108,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.STATIC,
|
type=FieldTypes.STATIC,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="static",
|
placeholder="static",
|
||||||
|
initial_value="something idk",
|
||||||
)
|
)
|
||||||
radio_button_group = Prompt.objects.create(
|
radio_button_group = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -106,6 +116,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
type=FieldTypes.RADIO_BUTTON_GROUP,
|
type=FieldTypes.RADIO_BUTTON_GROUP,
|
||||||
required=True,
|
required=True,
|
||||||
placeholder="test",
|
placeholder="test",
|
||||||
|
initial_value="test",
|
||||||
)
|
)
|
||||||
dropdown = Prompt.objects.create(
|
dropdown = Prompt.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -137,9 +148,9 @@ class TestPromptStage(FlowTestCase):
|
||||||
password_prompt.field_key: "test",
|
password_prompt.field_key: "test",
|
||||||
password2_prompt.field_key: "test",
|
password2_prompt.field_key: "test",
|
||||||
number_prompt.field_key: 3,
|
number_prompt.field_key: 3,
|
||||||
hidden_prompt.field_key: hidden_prompt.placeholder,
|
hidden_prompt.field_key: hidden_prompt.initial_value,
|
||||||
static_prompt.field_key: static_prompt.placeholder,
|
static_prompt.field_key: static_prompt.initial_value,
|
||||||
radio_button_group.field_key: radio_button_group.placeholder,
|
radio_button_group.field_key: radio_button_group.initial_value,
|
||||||
dropdown.field_key: "",
|
dropdown.field_key: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -335,106 +346,176 @@ class TestPromptStage(FlowTestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
|
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
|
||||||
)
|
)
|
||||||
context["text_prompt_expression"] = generate_id()
|
|
||||||
self.assertEqual(
|
def test_prompt_placeholder_does_not_take_value_from_context(self):
|
||||||
prompt.get_placeholder(context, self.user, self.factory.get("/")),
|
"""Test placeholder does not automatically take value from context"""
|
||||||
context["text_prompt_expression"],
|
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"]
|
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_choice_prompts_placeholders(self):
|
def test_prompt_initial_value(self):
|
||||||
"""Test placeholders and expression of choice fields"""
|
"""Test initial_value and expression"""
|
||||||
context = {"foo": generate_id()}
|
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(
|
prompt: Prompt = Prompt(
|
||||||
field_key="fixed_choice_prompt_expression",
|
field_key="fixed_choice_prompt_expression",
|
||||||
label="LABEL",
|
label="LABEL",
|
||||||
type=FieldTypes.RADIO_BUTTON_GROUP,
|
type=FieldTypes.RADIO_BUTTON_GROUP,
|
||||||
placeholder="return []",
|
placeholder="return []",
|
||||||
placeholder_expression=True,
|
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_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())
|
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(
|
prompt: Prompt = Prompt(
|
||||||
field_key="fixed_choice_prompt_expression",
|
field_key="fixed_choice_prompt_expression",
|
||||||
label="LABEL",
|
label="LABEL",
|
||||||
type=FieldTypes.DROPDOWN,
|
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,
|
placeholder_expression=True,
|
||||||
)
|
)
|
||||||
|
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
|
||||||
self.assertEqual(
|
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(
|
self.assertEqual(
|
||||||
prompt.get_choices(context, self.user, self.factory.get("/")),
|
prompt.get_choices(context, self.user, self.factory.get("/")),
|
||||||
(context["foo"], True, "text"),
|
("choice",),
|
||||||
)
|
|
||||||
context["fixed_choice_prompt_expression"] = tuple(["text", generate_id(), 2])
|
|
||||||
self.assertEqual(
|
|
||||||
prompt.get_placeholder(context, self.user, self.factory.get("/")),
|
|
||||||
"text",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
prompt.get_choices(context, self.user, self.factory.get("/")),
|
prompt.get_initial_value(context, self.user, self.factory.get("/")),
|
||||||
context["fixed_choice_prompt_expression"],
|
"choice",
|
||||||
)
|
|
||||||
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"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_choices_are_none_for_non_choice_fields(self):
|
def test_choices_are_none_for_non_choice_fields(self):
|
||||||
|
@ -505,6 +586,8 @@ class TestPromptStage(FlowTestCase):
|
||||||
"type": FieldTypes.TEXT,
|
"type": FieldTypes.TEXT,
|
||||||
"placeholder": 'return "Hello world"',
|
"placeholder": 'return "Hello world"',
|
||||||
"placeholder_expression": True,
|
"placeholder_expression": True,
|
||||||
|
"initial_value": 'return "Hello Hello world"',
|
||||||
|
"initial_value_expression": True,
|
||||||
"sub_text": "test",
|
"sub_text": "test",
|
||||||
"order": 123,
|
"order": 123,
|
||||||
},
|
},
|
||||||
|
@ -522,6 +605,7 @@ class TestPromptStage(FlowTestCase):
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"required": True,
|
"required": True,
|
||||||
"placeholder": "Hello world",
|
"placeholder": "Hello world",
|
||||||
|
"initial_value": "Hello Hello world",
|
||||||
"order": 123,
|
"order": 123,
|
||||||
"sub_text": "test",
|
"sub_text": "test",
|
||||||
"choices": None,
|
"choices": None,
|
||||||
|
|
|
@ -13,12 +13,14 @@ entries:
|
||||||
id: flow
|
id: flow
|
||||||
- attrs:
|
- attrs:
|
||||||
order: 200
|
order: 200
|
||||||
placeholder: |
|
placeholder: Username
|
||||||
|
placeholder_expression: false
|
||||||
|
initial_value: |
|
||||||
try:
|
try:
|
||||||
return user.username
|
return user.username
|
||||||
except:
|
except:
|
||||||
return ''
|
return ''
|
||||||
placeholder_expression: true
|
initial_value_expression: true
|
||||||
required: true
|
required: true
|
||||||
type: text
|
type: text
|
||||||
field_key: username
|
field_key: username
|
||||||
|
@ -29,12 +31,14 @@ entries:
|
||||||
model: authentik_stages_prompt.prompt
|
model: authentik_stages_prompt.prompt
|
||||||
- attrs:
|
- attrs:
|
||||||
order: 201
|
order: 201
|
||||||
placeholder: |
|
placeholder: Name
|
||||||
|
placeholder_expression: false
|
||||||
|
initial_value: |
|
||||||
try:
|
try:
|
||||||
return user.name
|
return user.name
|
||||||
except:
|
except:
|
||||||
return ''
|
return ''
|
||||||
placeholder_expression: true
|
initial_value_expression: true
|
||||||
required: true
|
required: true
|
||||||
type: text
|
type: text
|
||||||
field_key: name
|
field_key: name
|
||||||
|
@ -45,12 +49,14 @@ entries:
|
||||||
model: authentik_stages_prompt.prompt
|
model: authentik_stages_prompt.prompt
|
||||||
- attrs:
|
- attrs:
|
||||||
order: 202
|
order: 202
|
||||||
placeholder: |
|
placeholder: Email
|
||||||
|
placeholder_expression: false
|
||||||
|
initial_value: |
|
||||||
try:
|
try:
|
||||||
return user.email
|
return user.email
|
||||||
except:
|
except:
|
||||||
return ''
|
return ''
|
||||||
placeholder_expression: true
|
initial_value_expression: true
|
||||||
required: true
|
required: true
|
||||||
type: email
|
type: email
|
||||||
field_key: email
|
field_key: email
|
||||||
|
@ -61,12 +67,14 @@ entries:
|
||||||
model: authentik_stages_prompt.prompt
|
model: authentik_stages_prompt.prompt
|
||||||
- attrs:
|
- attrs:
|
||||||
order: 203
|
order: 203
|
||||||
placeholder: |
|
placeholder: Locale
|
||||||
|
placeholder_expression: false
|
||||||
|
initial_value: |
|
||||||
try:
|
try:
|
||||||
return user.attributes.get("settings", {}).get("locale", "")
|
return user.attributes.get("settings", {}).get("locale", "")
|
||||||
except:
|
except:
|
||||||
return ''
|
return ''
|
||||||
placeholder_expression: true
|
initial_value_expression: true
|
||||||
required: true
|
required: true
|
||||||
type: ak-locale
|
type: ak-locale
|
||||||
field_key: attributes.settings.locale
|
field_key: attributes.settings.locale
|
||||||
|
|
39
schema.yml
39
schema.yml
|
@ -36862,8 +36862,14 @@ components:
|
||||||
type: boolean
|
type: boolean
|
||||||
placeholder:
|
placeholder:
|
||||||
type: string
|
type: string
|
||||||
description: When creating a Radio Button Group or Dropdown, enable interpreting
|
description: Optionally provide a short hint that describes the expected
|
||||||
as expression and return a list to return multiple choices.
|
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:
|
order:
|
||||||
type: integer
|
type: integer
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
|
@ -36876,6 +36882,8 @@ components:
|
||||||
type: string
|
type: string
|
||||||
placeholder_expression:
|
placeholder_expression:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
initial_value_expression:
|
||||||
|
type: boolean
|
||||||
PatchedPromptStageRequest:
|
PatchedPromptStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: PromptStage Serializer
|
description: PromptStage Serializer
|
||||||
|
@ -38034,8 +38042,14 @@ components:
|
||||||
type: boolean
|
type: boolean
|
||||||
placeholder:
|
placeholder:
|
||||||
type: string
|
type: string
|
||||||
description: When creating a Radio Button Group or Dropdown, enable interpreting
|
description: Optionally provide a short hint that describes the expected
|
||||||
as expression and return a list to return multiple choices.
|
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:
|
order:
|
||||||
type: integer
|
type: integer
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
|
@ -38048,6 +38062,8 @@ components:
|
||||||
type: string
|
type: string
|
||||||
placeholder_expression:
|
placeholder_expression:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
initial_value_expression:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- field_key
|
- field_key
|
||||||
- label
|
- label
|
||||||
|
@ -38109,8 +38125,14 @@ components:
|
||||||
type: boolean
|
type: boolean
|
||||||
placeholder:
|
placeholder:
|
||||||
type: string
|
type: string
|
||||||
description: When creating a Radio Button Group or Dropdown, enable interpreting
|
description: Optionally provide a short hint that describes the expected
|
||||||
as expression and return a list to return multiple choices.
|
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:
|
order:
|
||||||
type: integer
|
type: integer
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
|
@ -38123,6 +38145,8 @@ components:
|
||||||
type: string
|
type: string
|
||||||
placeholder_expression:
|
placeholder_expression:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
initial_value_expression:
|
||||||
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- field_key
|
- field_key
|
||||||
- label
|
- label
|
||||||
|
@ -40267,6 +40291,8 @@ components:
|
||||||
type: boolean
|
type: boolean
|
||||||
placeholder:
|
placeholder:
|
||||||
type: string
|
type: string
|
||||||
|
initial_value:
|
||||||
|
type: string
|
||||||
order:
|
order:
|
||||||
type: integer
|
type: integer
|
||||||
sub_text:
|
sub_text:
|
||||||
|
@ -40279,6 +40305,7 @@ components:
|
||||||
required:
|
required:
|
||||||
- choices
|
- choices
|
||||||
- field_key
|
- field_key
|
||||||
|
- initial_value
|
||||||
- label
|
- label
|
||||||
- order
|
- order
|
||||||
- placeholder
|
- placeholder
|
||||||
|
|
|
@ -372,8 +372,8 @@ export class PromptForm extends ModelForm<Prompt, string> {
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${t`When checked, the placeholder will be evaluated in the same way environment as a property mapping.
|
${t`When checked, the placeholder will be evaluated in the same way a property mapping is.
|
||||||
If the evaluation failed, the placeholder itself is returned.`}
|
If the evaluation fails, the placeholder itself is returned.`}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal label=${t`Placeholder`} name="placeholder">
|
<ak-form-element-horizontal label=${t`Placeholder`} name="placeholder">
|
||||||
|
@ -386,11 +386,41 @@ export class PromptForm extends ModelForm<Prompt, string> {
|
||||||
>
|
>
|
||||||
</ak-codemirror>
|
</ak-codemirror>
|
||||||
<p class="pf-c-form__helper-text">
|
<p class="pf-c-form__helper-text">
|
||||||
${t`Optionally pre-fill the input value.
|
${t`Optionally provide a short hint that describes the expected input value.
|
||||||
When creating a "Radio Button Group" or "Dropdown", enable interpreting as
|
When creating a fixed choice field, enable interpreting as
|
||||||
expression and return a list to return multiple choices.`}
|
expression and return a list to return multiple choices.`}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</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-form-element-horizontal label=${t`Help text`} name="subText">
|
||||||
<ak-codemirror
|
<ak-codemirror
|
||||||
mode="htmlmixed"
|
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) {
|
switch (prompt.type) {
|
||||||
case PromptTypeEnum.Text:
|
case PromptTypeEnum.Text:
|
||||||
return `<input
|
return `<input
|
||||||
|
@ -58,7 +58,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${prompt.required}
|
?required=${prompt.required}
|
||||||
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
|
value="${prompt.initialValue}">`;
|
||||||
case PromptTypeEnum.TextArea:
|
case PromptTypeEnum.TextArea:
|
||||||
return `<textarea
|
return `<textarea
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -67,21 +67,23 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${prompt.required}
|
?required=${prompt.required}
|
||||||
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
|
value="${prompt.initialValue}"">`;
|
||||||
case PromptTypeEnum.TextReadOnly:
|
case PromptTypeEnum.TextReadOnly:
|
||||||
return `<input
|
return `<input
|
||||||
type="text"
|
type="text"
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
|
placeholder="${prompt.placeholder}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
readonly
|
readonly
|
||||||
value="${prompt.placeholder}">`;
|
value="${prompt.initialValue}">`;
|
||||||
case PromptTypeEnum.TextAreaReadOnly:
|
case PromptTypeEnum.TextAreaReadOnly:
|
||||||
return `<textarea
|
return `<textarea
|
||||||
type="text"
|
type="text"
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
|
placeholder="${prompt.placeholder}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
readonly
|
readonly
|
||||||
value="${prompt.placeholder}">`;
|
value="${prompt.initialValue}">`;
|
||||||
case PromptTypeEnum.Username:
|
case PromptTypeEnum.Username:
|
||||||
return `<input
|
return `<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -90,7 +92,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${prompt.required}
|
?required=${prompt.required}
|
||||||
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
|
value="${prompt.initialValue}">`;
|
||||||
case PromptTypeEnum.Email:
|
case PromptTypeEnum.Email:
|
||||||
return `<input
|
return `<input
|
||||||
type="email"
|
type="email"
|
||||||
|
@ -98,7 +100,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
placeholder="${prompt.placeholder}"
|
placeholder="${prompt.placeholder}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${prompt.required}
|
?required=${prompt.required}
|
||||||
value="${placeholderAsValue ? prompt.placeholder : ""}">`;
|
value="${prompt.initialValue}">`;
|
||||||
case PromptTypeEnum.Password:
|
case PromptTypeEnum.Password:
|
||||||
return `<input
|
return `<input
|
||||||
type="password"
|
type="password"
|
||||||
|
@ -113,46 +115,50 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
placeholder="${prompt.placeholder}"
|
placeholder="${prompt.placeholder}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${prompt.required}>`;
|
?required=${prompt.required}
|
||||||
|
value="${prompt.initialValue}">`;
|
||||||
case PromptTypeEnum.Date:
|
case PromptTypeEnum.Date:
|
||||||
return `<input
|
return `<input
|
||||||
type="date"
|
type="date"
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
placeholder="${prompt.placeholder}"
|
placeholder="${prompt.placeholder}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${prompt.required}>`;
|
?required=${prompt.required}
|
||||||
|
value="${prompt.initialValue}">`;
|
||||||
case PromptTypeEnum.DateTime:
|
case PromptTypeEnum.DateTime:
|
||||||
return `<input
|
return `<input
|
||||||
type="datetime"
|
type="datetime"
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
placeholder="${prompt.placeholder}"
|
placeholder="${prompt.placeholder}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${prompt.required}>`;
|
?required=${prompt.required}
|
||||||
|
value="${prompt.initialValue}">`;
|
||||||
case PromptTypeEnum.File:
|
case PromptTypeEnum.File:
|
||||||
return `<input
|
return `<input
|
||||||
type="file"
|
type="file"
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
placeholder="${prompt.placeholder}"
|
placeholder="${prompt.placeholder}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${prompt.required}>`;
|
?required=${prompt.required}
|
||||||
|
value="${prompt.initialValue}">`;
|
||||||
case PromptTypeEnum.Separator:
|
case PromptTypeEnum.Separator:
|
||||||
return `<ak-divider>${prompt.placeholder}</ak-divider>`;
|
return `<ak-divider>${prompt.placeholder}</ak-divider>`;
|
||||||
case PromptTypeEnum.Hidden:
|
case PromptTypeEnum.Hidden:
|
||||||
return `<input
|
return `<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
value="${prompt.placeholder}"
|
value="${prompt.initialValue}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
?required=${prompt.required}>`;
|
?required=${prompt.required}>`;
|
||||||
case PromptTypeEnum.Static:
|
case PromptTypeEnum.Static:
|
||||||
return `<p>${prompt.placeholder}</p>`;
|
return `<p>${prompt.initialValue}</p>`;
|
||||||
case PromptTypeEnum.Dropdown:
|
case PromptTypeEnum.Dropdown:
|
||||||
return `<select class="pf-c-form-control" name="${prompt.fieldKey}">
|
return `<select class="pf-c-form-control" name="${prompt.fieldKey}">
|
||||||
${prompt.choices
|
${prompt.choices
|
||||||
?.map((choice) => {
|
?.map((choice) => {
|
||||||
return `<option
|
return `<option
|
||||||
value="${choice}"
|
value="${choice}"
|
||||||
?selected=${prompt.placeholder === choice}
|
?selected=${prompt.initialValue === choice}
|
||||||
>
|
>
|
||||||
${choice}
|
${choice}
|
||||||
</option>`;
|
</option>`;
|
||||||
|
@ -168,7 +174,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
type="radio"
|
type="radio"
|
||||||
class="pf-c-check__input"
|
class="pf-c-check__input"
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
checked="${prompt.placeholder === choice}"
|
checked="${prompt.initialValue === choice}"
|
||||||
required="${prompt.required}"
|
required="${prompt.required}"
|
||||||
value="${choice}"
|
value="${choice}"
|
||||||
/>
|
/>
|
||||||
|
@ -180,7 +186,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
);
|
);
|
||||||
case PromptTypeEnum.AkLocale:
|
case PromptTypeEnum.AkLocale:
|
||||||
return `<select class="pf-c-form-control" name="${prompt.fieldKey}">
|
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)`}
|
${t`Auto-detect (based on your browser)`}
|
||||||
</option>
|
</option>
|
||||||
${LOCALES.filter((locale) => {
|
${LOCALES.filter((locale) => {
|
||||||
|
@ -195,7 +201,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
.map((locale) => {
|
.map((locale) => {
|
||||||
return `<option
|
return `<option
|
||||||
value=${locale.code}
|
value=${locale.code}
|
||||||
${prompt.placeholder === locale.code ? "selected" : ""}
|
${prompt.initialValue === locale.code ? "selected" : ""}
|
||||||
>
|
>
|
||||||
${locale.code.toUpperCase()} - ${locale.label}
|
${locale.code.toUpperCase()} - ${locale.label}
|
||||||
</option>`;
|
</option>`;
|
||||||
|
@ -234,7 +240,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="pf-c-check__input"
|
class="pf-c-check__input"
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
?checked=${prompt.placeholder !== ""}
|
?checked=${prompt.initialValue !== ""}
|
||||||
?required=${prompt.required}
|
?required=${prompt.required}
|
||||||
/>
|
/>
|
||||||
<label class="pf-c-check__label">${prompt.label}</label>
|
<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"
|
class="pf-c-form__group"
|
||||||
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
|
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
|
||||||
>
|
>
|
||||||
${unsafeHTML(this.renderPromptInner(prompt, false))}
|
${unsafeHTML(this.renderPromptInner(prompt))} ${this.renderPromptHelpText(prompt)}
|
||||||
${this.renderPromptHelpText(prompt)}
|
|
||||||
</ak-form-element>`;
|
</ak-form-element>`;
|
||||||
}
|
}
|
||||||
return html` ${unsafeHTML(this.renderPromptInner(prompt, false))}
|
return html` ${unsafeHTML(this.renderPromptInner(prompt))}
|
||||||
${this.renderPromptHelpText(prompt)}`;
|
${this.renderPromptHelpText(prompt)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ export class UserSettingsPromptStage extends PromptStage {
|
||||||
return super.styles.concat(PFCheck);
|
return super.styles.concat(PFCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPromptInner(prompt: StagePrompt, placeholderAsValue: boolean): string {
|
renderPromptInner(prompt: StagePrompt): string {
|
||||||
switch (prompt.type) {
|
switch (prompt.type) {
|
||||||
// Checkbox requires slightly different rendering here due to the use of horizontal form elements
|
// Checkbox requires slightly different rendering here due to the use of horizontal form elements
|
||||||
case PromptTypeEnum.Checkbox:
|
case PromptTypeEnum.Checkbox:
|
||||||
|
@ -25,12 +25,12 @@ export class UserSettingsPromptStage extends PromptStage {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="pf-c-check__input"
|
class="pf-c-check__input"
|
||||||
name="${prompt.fieldKey}"
|
name="${prompt.fieldKey}"
|
||||||
?checked=${prompt.placeholder !== ""}
|
?checked=${prompt.initialValue !== ""}
|
||||||
?required=${prompt.required}
|
?required=${prompt.required}
|
||||||
style="vertical-align: bottom"
|
style="vertical-align: bottom"
|
||||||
/>`;
|
/>`;
|
||||||
default:
|
default:
|
||||||
return super.renderPromptInner(prompt, placeholderAsValue);
|
return super.renderPromptInner(prompt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,13 +47,13 @@ export class UserSettingsPromptStage extends PromptStage {
|
||||||
return error.string;
|
return error.string;
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
${unsafeHTML(this.renderPromptInner(prompt, true))}
|
${unsafeHTML(this.renderPromptInner(prompt))}
|
||||||
${this.renderPromptHelpText(prompt)}
|
${this.renderPromptHelpText(prompt)}
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return html`
|
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.
|
- _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
|
- _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.
|
- _Radio Button Group_ and _Dropdown_: Only allow the user to select one of a set of predefined values.
|
||||||
|
|
||||||
A prompt has the following attributes:
|
A prompt has the following attributes:
|
||||||
|
@ -60,15 +60,34 @@ A flag which decides whether or not this field is required.
|
||||||
|
|
||||||
### `placeholder`
|
### `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
|
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).
|
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.
|
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`
|
### `order`
|
||||||
|
|
||||||
The numerical index of the prompt. This applies to all stages which this prompt is a part of.
|
The numerical index of the prompt. This applies to all stages which this prompt is a part of.
|
||||||
|
|
Reference in New Issue