Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add the ability to set a form as non applicable by default #3551

Merged
merged 10 commits into from
Oct 30, 2023
11 changes: 11 additions & 0 deletions src/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7422,6 +7422,9 @@ components:
type: string
format: uri
readOnly: true
isApplicable:
type: boolean
description: Whether the step is applicable by default.
loginRequired:
type: boolean
readOnly: true
Expand Down Expand Up @@ -7763,6 +7766,7 @@ components:
disable-next: '#/components/schemas/LogicActionPolymorphicGenericObject'
property: '#/components/schemas/LogicActionPolymorphicLogicPropertyAction'
step-not-applicable: '#/components/schemas/LogicActionPolymorphicGenericObject'
step-applicable: '#/components/schemas/LogicActionPolymorphicGenericObject'
variable: '#/components/schemas/LogicActionPolymorphicLogicValueAction'
fetch-from-service: '#/components/schemas/LogicActionPolymorphicLogicFetchAction'
set-registration-backend: '#/components/schemas/LogicActionPolymorphicLogicSetRegistrationBackendAction'
Expand Down Expand Up @@ -7816,6 +7820,7 @@ components:
LogicActionPolymorphicSharedTypeEnum:
enum:
- step-not-applicable
- step-applicable
- disable-next
- property
- variable
Expand Down Expand Up @@ -8003,6 +8008,9 @@ components:
url:
type: string
format: uri
isApplicable:
type: boolean
description: Whether the step is applicable by default.
required:
- formDefinition
- index
Expand Down Expand Up @@ -8421,6 +8429,9 @@ components:
type: string
format: uri
readOnly: true
isApplicable:
type: boolean
description: Whether the step is applicable by default.
loginRequired:
type: boolean
readOnly: true
Expand Down
5 changes: 5 additions & 0 deletions src/openforms/forms/api/serializers/form_step.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from ...models import FormDefinition, FormStep
from ...validators import validate_no_duplicate_keys_across_steps
from ..validators import FormStepIsApplicableIfFirstValidator
from .button_text import ButtonTextSerializer


Expand Down Expand Up @@ -54,6 +55,7 @@ class Meta:
"index",
"literals",
"url",
"is_applicable",
)
extra_kwargs = {
"uuid": {
Expand Down Expand Up @@ -108,6 +110,7 @@ class Meta:
"name",
"internal_name",
"url",
"is_applicable",
"login_required",
"is_reusable",
"literals",
Expand All @@ -123,6 +126,7 @@ class Meta:
"name",
"internal_name",
"url",
"is_applicable",
"login_required",
"is_reusable",
"literals",
Expand All @@ -138,6 +142,7 @@ class Meta:
"read_only": True,
},
}
validators = [FormStepIsApplicableIfFirstValidator()]

def create(self, validated_data):
validated_data["form"] = self.context["form"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class LogicActionPolymorphicSerializer(PolymorphicSerializer):
str(LogicActionTypes.disable_next): DummySerializer,
str(LogicActionTypes.property): LogicPropertyActionSerializer,
str(LogicActionTypes.step_not_applicable): DummySerializer,
str(LogicActionTypes.step_applicable): DummySerializer,
str(LogicActionTypes.variable): LogicValueActionSerializer,
str(LogicActionTypes.fetch_from_service): LogicFetchActionSerializer,
str(
Expand Down
13 changes: 13 additions & 0 deletions src/openforms/forms/api/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,19 @@ def __call__(self, attrs: dict, serializer: serializers.Serializer):
)


class FormStepIsApplicableIfFirstValidator:
def __call__(self, attrs: dict):
if not attrs.get("is_applicable", True) and attrs.get("order") == 0:
SilviaAmAm marked this conversation as resolved.
Show resolved Hide resolved
raise serializers.ValidationError(
{
"is_applicable": serializers.ErrorDetail(
_("First form step must be applicable."),
code="invalid",
),
}
)


def validate_template_expressions(configuration: JSONObject) -> None:
"""
Validate that any template expressions in supported properties are correct.
Expand Down
1 change: 1 addition & 0 deletions src/openforms/forms/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class LogicActionTypes(models.TextChoices):
step_not_applicable = "step-not-applicable", _(
"Mark the form step as not-applicable"
)
step_applicable = "step-applicable", _("Mark the form step as applicable")
disable_next = "disable-next", _("Disable the next step")
property = "property", _("Modify a component property")
variable = "variable", _("Set the value of a variable")
Expand Down
22 changes: 22 additions & 0 deletions src/openforms/forms/migrations/0097_formstep_is_applicable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 3.2.21 on 2023-10-23 12:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("forms", "0096_move_time_component_validators"),
]

operations = [
migrations.AddField(
model_name="formstep",
name="is_applicable",
field=models.BooleanField(
default=True,
help_text="Whether the step is applicable by default.",
verbose_name="is applicable",
),
),
]
14 changes: 14 additions & 0 deletions src/openforms/forms/models/form_step.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import uuid

from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -61,6 +62,11 @@ class FormStep(OrderedModel):
"Leave blank to get value from global configuration."
),
)
is_applicable = models.BooleanField(
_("is applicable"),
default=True,
help_text=_("Whether the step is applicable by default."),
)

order_with_respect_to = "form"

Expand Down Expand Up @@ -102,3 +108,11 @@ def delete(self, *args, **kwargs):

def iter_components(self, recursive=True, **kwargs):
yield from self.form_definition.iter_components(recursive=recursive, **kwargs)

def clean(self) -> None:
if not self.is_applicable and self.order == 0:
raise ValidationError(
{"is_applicable": _("First form step must be applicable.")},
code="invalid",
)
sergei-maertens marked this conversation as resolved.
Show resolved Hide resolved
return super().clean()
42 changes: 42 additions & 0 deletions src/openforms/forms/tests/test_api_formsteps.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,3 +1092,45 @@ def test_update_with_translations_validate_literals(self, _mock):
for error in expected:
with self.subTest(field=error["name"], code=error["code"]):
self.assertIn(error, invalid_params)


class FormStepsAPIApplicabilityTests(APITestCase):
def test_create_form_step_not_applicable_as_first_unsucessful(self):
user = UserFactory.create()
form = FormFactory.create()
form_definition = FormDefinitionFactory.create()
self.client.force_authenticate(user=user)
user.user_permissions.add(Permission.objects.get(codename="change_form"))
user.is_staff = True
user.save()
url = reverse("api:form-steps-list", kwargs={"form_uuid_or_slug": form.uuid})

form_detail_url = reverse(
"api:formdefinition-detail",
kwargs={"uuid": form_definition.uuid},
)
data = {
"formDefinition": f"http://testserver{form_detail_url}",
"index": 0,
"isApplicable": False,
}
response = self.client.post(url, data=data)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.json()["invalidParams"][0],
{
"name": "isApplicable",
"code": "invalid",
"reason": "First form step must be applicable.",
},
)

data = {
"formDefinition": f"http://testserver{form_detail_url}",
"index": 0,
"isApplicable": True,
}
response = self.client.post(url, data=data)

self.assertEqual(response.status_code, status.HTTP_201_CREATED)
19 changes: 19 additions & 0 deletions src/openforms/forms/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,25 @@ def test_str_no_relation(self):
step = FormStep()
self.assertEqual(str(step), "FormStep object (None)")

def test_clean(self):
step_raises = FormStepFactory.create(
order=0,
is_applicable=False,
)
step_ok = FormStepFactory.create(
order=0,
is_applicable=True,
)
Viicos marked this conversation as resolved.
Show resolved Hide resolved
step_ok_order_1 = FormStepFactory.create(
order=1,
is_applicable=False,
)
with self.subTest("clean raises"):
self.assertRaises(ValidationError, step_raises.clean)
with self.subTest("clean does not raise"):
step_ok.clean()
step_ok_order_1.clean()


class FormLogicTests(TestCase):
def test_block_form_logic_trigger_step_other_form(self):
Expand Down
4 changes: 4 additions & 0 deletions src/openforms/js/components/admin/form_design/FormStep.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import TYPES from './types';
const FormStep = ({data, onEdit, onComponentMutated, onFieldChange, onReplace}) => {
const {
_generatedId,
index,
configuration,
formDefinition,
name,
internalName,
slug,
loginRequired,
translations,
isApplicable,
isReusable,
isNew,
validationErrors = [],
Expand All @@ -39,12 +41,14 @@ const FormStep = ({data, onEdit, onComponentMutated, onFieldChange, onReplace})
return (
<FormStepDefinition
internalName={internalName}
index={index}
slug={slug}
url={formDefinition}
generatedId={_generatedId}
translations={translations}
componentTranslations={componentTranslations}
configuration={configuration}
isApplicable={isApplicable}
loginRequired={loginRequired}
isReusable={isReusable}
onFieldChange={onFieldChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ const FormStepDefinition = ({
url = '',
generatedId = '',
internalName = '',
index = null,
slug = '',
isApplicable = true,
loginRequired = false,
isReusable = false,
translations = {},
Expand Down Expand Up @@ -317,6 +319,28 @@ const FormStepDefinition = ({
/>
</Field>
</FormRow>
<FormRow>
<Field
name="isApplicable"
errorClassPrefix={'checkbox'}
errorClassModifier={'no-padding'}
>
<Checkbox
label={
<FormattedMessage
defaultMessage="Is applicable?"
description="Form step is applicable label"
/>
}
name="isApplicable"
checked={isApplicable}
onChange={e =>
onFieldChange({target: {name: 'isApplicable', value: !isApplicable}})
}
disabled={index === 0 || langCode !== defaultLang} // First step can't be n/a by default
/>
</Field>
</FormRow>
<FormRow>
<Field
name="loginRequired"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const updateOrCreateSingleFormStep = async (
const stepData = {
index: index,
slug: step.slug,
isApplicable: step.isApplicable,
formDefinition: definitionResponse.data.url,
translations: formStepTranslations,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ const ActionStepNotApplicable = ({action, errors, onChange}) => {
);
};

const ActionStepApplicable = ({action, errors, onChange}) => {
return (
<DSLEditorNode errors={errors.formStepUuid}>
<StepSelection name="formStepUuid" value={action.formStepUuid} onChange={onChange} />
</DSLEditorNode>
);
};

const ActionSetRegistrationBackend = ({action, errors, onChange}) => {
return (
<DSLEditorNode errors={errors.value}>
Expand Down Expand Up @@ -240,6 +248,10 @@ const ActionComponent = ({action, errors, onChange}) => {
Component = ActionStepNotApplicable;
break;
}
case 'step-applicable': {
Component = ActionStepApplicable;
break;
}
case 'set-registration-backend': {
Component = ActionSetRegistrationBackend;
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ const ACTION_TYPES = [
defaultMessage: 'Mark the form step as not-applicable',
}),
],
[
'step-applicable',
defineMessage({
description: 'action type "step-applicable" label',
defaultMessage: 'Mark the form step as applicable',
}),
],
[
'set-registration-backend',
defineMessage({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const FormStep = PropTypes.shape({
name: PropTypes.string,
internalName: PropTypes.string,
slug: PropTypes.string,
isApplicable: PropTypes.bool,
loginRequired: PropTypes.bool,
isReusable: PropTypes.bool,
url: PropTypes.string,
Expand Down
Loading
Loading