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:
		| @ -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: | ||||
|             # Choice fields use the placeholder to define all valid choices. | ||||
|             # Therefore their actual placeholder is always blank | ||||
|             return "" | ||||
|             return choices[0] | ||||
|  | ||||
|         if self.field_key in prompt_context: | ||||
|             # We don't want to parse this as an expression since a user will | ||||
|             # be able to control the input | ||||
|             return prompt_context[self.field_key] | ||||
|  | ||||
|         if self.placeholder_expression: | ||||
|             evaluator = PropertyMappingEvaluator( | ||||
| @ -211,6 +216,47 @@ class Prompt(SerializerModel): | ||||
|                     raise wrapped from exc | ||||
|         return self.placeholder | ||||
|  | ||||
|     def get_initial_value( | ||||
|         self, | ||||
|         prompt_context: dict, | ||||
|         user: User, | ||||
|         request: HttpRequest, | ||||
|         dry_run: Optional[bool] = False, | ||||
|     ) -> str: | ||||
|         """Get fully interpolated initial value""" | ||||
|  | ||||
|         if self.field_key in prompt_context: | ||||
|             # We don't want to parse this as an expression since a user will | ||||
|             # be able to control the input | ||||
|             value = prompt_context[self.field_key] | ||||
|         elif self.initial_value_expression: | ||||
|             evaluator = PropertyMappingEvaluator( | ||||
|                 self, user, request, prompt_context=prompt_context, dry_run=dry_run | ||||
|             ) | ||||
|             try: | ||||
|                 value = evaluator.evaluate(self.initial_value) | ||||
|             except Exception as exc:  # pylint:disable=broad-except | ||||
|                 wrapped = PropertyMappingExpressionException(str(exc)) | ||||
|                 LOGGER.warning( | ||||
|                     "failed to evaluate prompt initial value", | ||||
|                     exc=wrapped, | ||||
|                 ) | ||||
|                 if dry_run: | ||||
|                     raise wrapped from exc | ||||
|                 value = self.initial_value | ||||
|         else: | ||||
|             value = self.initial_value | ||||
|  | ||||
|         if self.type in CHOICE_FIELDS: | ||||
|             # Ensure returned value is a valid choice | ||||
|             choices = self.get_choices(prompt_context, user, request) | ||||
|             if not choices: | ||||
|                 return "" | ||||
|             if value not in choices: | ||||
|                 return choices[0] | ||||
|  | ||||
|         return value | ||||
|  | ||||
|     def field(self, default: Optional[Any], choices: Optional[list[Any]] = None) -> CharField: | ||||
|         """Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS.""" | ||||
|         field_class = CharField | ||||
|  | ||||
| @ -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
	
	Block a user
	 sdimovv
					sdimovv