Skip to content

Commit

Permalink
Merge pull request #3551 from open-formulieren/feature/2952-steps-app…
Browse files Browse the repository at this point in the history
…licable

✨ Add the ability to set a form as non applicable by default
  • Loading branch information
sergei-maertens authored Oct 30, 2023
2 parents 38d3748 + badfe84 commit df68b79
Show file tree
Hide file tree
Showing 19 changed files with 305 additions and 4 deletions.
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:
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",
)
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,
)
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

0 comments on commit df68b79

Please sign in to comment.