Пример #1
0
def test_field() -> None:
    """Ensure that fields can be created within a form."""
    field = FieldFactory.build(
        form=FormFactory(),
        label=None,
        field_type=SingleLineTextField.name,
    )

    # Ensure that the field has no machine name until it gets saved to the
    # database.
    assert field.name == ""

    # Ensure the field has a friendly string representation
    assert str(field) == "New App Field"

    # Ensure that a machine name is generated for the field on save.
    field.label = "Test Field"
    field.save()
    assert field.name == "test_field"

    # Ensure that updating the field label does not change the name,
    # which should remain stable.
    field.label = "Updated Test Field"
    field.save()
    assert field.name == "test_field"

    # Ensure the field's friendly string representation reflects the label
    assert str(field) == "Updated Test Field"

    # Ensure that a Django form field instance can be produced from the field.
    assert isinstance(field.as_form_field(field_values={}), forms.Field)

    # Ensure that a Django model field instance can be produced from the field.
    assert isinstance(field.as_model_field(), models.Field)
Пример #2
0
def test_noop_modifier_attribute() -> None:
    """Ensure that a nonexistent attribute in a modifier is a noop.

    If a FieldModifier modifies an attribute that does not exist on the
    field and has no explicit handler on the field type, its only effect
    should be that it appears in the "_modifiers" dict on the rendered
    form field.
    """
    form = FormFactory(label="Orphan Field Modifier")

    # Define a field that has a modifier that tries to change a nonexistent attribute.
    field = FieldFactory(
        form=form,
        label="Are we testing?",
        name="test_field",
        field_type=YesNoRadioField.name,
        required=True,
    )
    modifier = field.modifiers.create(
        attribute="noop_modifier",
        expression=f"empty({field.name})",
    )

    django_form = form.as_django_form()

    # The only effect an unhandled modifier should have is to be present in the
    # modifiers dict.
    assert modifier.attribute in django_form.fields[field.name]._modifiers
Пример #3
0
def test_initial_values() -> None:
    """Ensure initial values are respected for django forms."""
    form = FormFactory(label="Initial Value Form")

    # Define a field that has a modifier that tries to change a nonexistent attribute.
    field = FieldFactory(
        form=form,
        label="How many?",
        name="test_field",
        field_type=IntegerField.name,
        required=True,
        initial=0,
    )
    django_form = form.as_django_form()
    assert not django_form.is_bound
    assert django_form.initial.get(field.name) == field.initial

    django_form = form.as_django_form(data={})
    assert django_form.is_bound
    assert django_form.initial.get(field.name) == field.initial

    django_form = form.as_django_form(initial={field.name: 123})
    assert not django_form.is_bound
    assert django_form.initial.get(field.name) == 123

    django_form = form.as_django_form(data={}, initial={field.name: 123})
    assert django_form.is_bound
    assert django_form.initial.get(field.name) == 123
Пример #4
0
def test_file_upload() -> None:
    """Ensure that file uploads are handled correctly."""
    form = FormFactory(label="File Upload Form")

    # Define a field that has a modifier that tries to change a nonexistent attribute.
    file_field = FieldFactory(
        form=form,
        label="Upload a file",
        name="file",
        field_type=FileUploadField.name,
        required=False,
    )

    # Upload a file.
    uploaded_file = SimpleUploadedFile(
        "random-file.txt",
        b"Hello, world!",
        content_type="text/plain",
    )

    # Generate a Django form.
    django_form = form.as_django_form(files={file_field.name: uploaded_file})

    # The form should be valid, and saving it should produce a record.
    assert django_form.is_valid(), (
        django_form.errors,
        django_form.data,
        django_form.initial,
    )
    record = django_form.save()
    assert isinstance(record.file, File)

    # Setting the field to False should result in a null value when cleaned.
    django_form = form.as_django_form(files={file_field.name: False},
                                      instance=record)
    assert django_form.is_valid(), (django_form.errors, django_form.data)
    record = django_form.save()
    assert record.file._file is None
Пример #5
0
def test_record_queries(django_assert_num_queries) -> None:
    """Ensure that a minimal number of queries is required to fetch records."""
    forms_count = 3
    fields_per_form_count = len(FIELD_TYPES)
    records_per_form_count = 1

    forms = FormFactory.create_batch(forms_count)

    for form in forms:
        AppField.objects.bulk_create(
            FieldFactory.build(form=form,
                               name=f"{field_type}_field",
                               field_type=field_type,
                               _order=0) for field_type in FIELD_TYPES.keys())

        record = AppRecord.objects.create(form=form)
        for field in form.fields.all():
            setattr(record, field.name, None)
        record.save()

    # There should be `forms_count` forms in the database.
    assert AppForm.objects.count() == forms_count

    # There should be `forms_count * fields_per_form_count` fields in the database (one field per type in FIELD_TYPES).
    assert AppField.objects.count() == forms_count * fields_per_form_count

    # There should be `forms_count * records_per_form_count` records in the database.
    assert AppRecord.objects.count() == forms_count * records_per_form_count

    # There should be `forms_count * fields_per_form_count` records in the database (one record per form, one field per type in FIELD_TYPES).
    assert AppRecordAttribute.objects.count(
    ) == forms_count * fields_per_form_count

    # By default, the records QuerySet should only require these queries do
    # perform all of the core responsibilities of the library from the
    # perspective of Record instances (which is what most implementations will
    # be interacting with most of the time in e.g. views):
    #
    #   1. One to fetch the list of records; this should include a prefetch of its form.
    #   2. One to fetch the list of the fields on the record's form.
    #   3. One to fetch the list of field modifiers fields on the record's form.
    #   4. One to fetch the list of fieldsets for each record's form.
    #   5. One to fetch the list of attributes for all of the records.
    #
    with django_assert_num_queries(5):
        records = cast(List[BaseRecord], list(AppRecord.objects.all()))

    # Fetching the data from any of the records should not require any
    # additional queries.
    with django_assert_num_queries(0):
        for record in records:
            assert record._data != {}

    # Validating an existing record against its Django form with no changes
    # should require only the queries necessary to fetch the form structure.
    with django_assert_num_queries(2):
        record = records[0]
        django_form = record.form.as_django_form(data={}, instance=record)
        assert django_form.is_valid(), django_form.errors

    # Updating a record using a Django form should require these queries:
    #
    #   * A SAVEPOINT query before saving the model.
    #   * Two queries to fetch the form structure for the record.
    #   * A single query to update the Record itself.
    #   * A single bulk_update query to update the attributes that have changed.
    #   * A RELEASE SAVEPOINT query after all of the queries have been executed.
    #
    with django_assert_num_queries(6):
        record = records[1]
        new_record_values = {
            f"{SingleLineTextField.name}_field": "new_value",
            f"{MultiLineTextField.name}_field": "another\nnew\nvalue",
        }
        django_form = record.form.as_django_form(instance=record,
                                                 data=new_record_values)
        assert django_form.is_valid(), django_form.errors

        updated_record = django_form.save()

        for attr, value in new_record_values.items():
            assert getattr(updated_record, attr) == value
Пример #6
0
def test_form_lifecycle() -> None:
    """Ensure that changing form values can change the form structure."""
    form = FormFactory(label="Bridgekeeper")

    name_field = FieldFactory(
        form=form,
        label="What... is your name?",
        name="name",
        field_type=SingleLineTextField.name,
        required=True,
    )

    # Define a field that is only visible and required if the name field is not
    # empty.
    quest_field = FieldFactory(
        form=form,
        label="What... is your quest?",
        name="quest",
        field_type=MultiLineTextField.name,
        required=True,
    )
    quest_field.modifiers.create(
        attribute="hidden",
        expression=f"empty({name_field.name})",
    )

    # Define a field that is only visible and required if the name field is not
    # empty.
    favorite_color_field = FieldFactory(
        form=form,
        label="What... is your favorite color?",
        name="favorite_color",
        field_type=SingleChoiceSelectField.name,
        form_field_options={
            "choices": (
                ("blue", "Blue"),
                ("yellow", "Yellow"),
            ),
        },
        required=True,
    )
    favorite_color_field.modifiers.create(
        attribute="hidden",
        expression="empty(quest)",
    )
    favorite_color_field.modifiers.create(
        attribute="help_text",
        expression="'Auuugh!' if favorite_color == 'yellow' else ''",
    )

    # Initially, the form should have three fields. Only the first field should
    # be visible and required.
    #
    # Since the first field is required, the form should not be valid since we
    # haven't provided a value for it.
    field_values = {}
    django_form = form.as_django_form(data=field_values)

    form_fields = django_form.fields
    assert form_fields["name"].required
    assert isinstance(form_fields["name"].widget, TextInput)
    assert not form_fields["quest"].required
    assert isinstance(form_fields["quest"].widget, HiddenInput)
    assert not form_fields["favorite_color"].required
    assert isinstance(form_fields["favorite_color"].widget, HiddenInput)

    assert not django_form.is_valid()
    assert "name" in django_form.errors

    # Filling out the first field should cause the second field to become
    # visible and required.
    field_values = {
        **field_values,
        "name": "Sir Lancelot of Camelot",
    }
    django_form = form.as_django_form(field_values)

    form_fields = django_form.fields
    assert form_fields["name"].required
    assert isinstance(form_fields["name"].widget, TextInput)
    assert form_fields["quest"].required
    assert isinstance(form_fields["quest"].widget, Textarea)
    assert not form_fields["favorite_color"].required
    assert isinstance(form_fields["favorite_color"].widget, HiddenInput)

    assert not django_form.is_valid()
    assert "quest" in django_form.errors

    # Filling out the second field should expose the last field.
    field_values = {
        **field_values,
        "quest": "To seek the Holy Grail.",
    }
    django_form = form.as_django_form(field_values)

    form_fields = django_form.fields
    assert form_fields["name"].required
    assert isinstance(form_fields["name"].widget, TextInput)
    assert form_fields["quest"].required
    assert isinstance(form_fields["quest"].widget, Textarea)
    assert form_fields["favorite_color"].required
    assert isinstance(form_fields["favorite_color"].widget, Select)

    assert not django_form.is_valid()
    assert "favorite_color" in django_form.errors

    # Filling out the last field with the "wrong" answer should change its help text.
    field_values = {
        **field_values,
        "favorite_color": "yellow",
    }
    django_form = form.as_django_form(field_values)

    form_fields = django_form.fields
    assert form_fields["name"].required
    assert isinstance(form_fields["name"].widget, TextInput)
    assert form_fields["quest"].required
    assert isinstance(form_fields["quest"].widget, Textarea)
    assert form_fields["favorite_color"].required
    assert form_fields["favorite_color"].help_text == "Auuugh!"
    assert isinstance(form_fields["favorite_color"].widget, Select)

    # A completely filled-out form should be valid.
    assert django_form.is_valid(), (
        django_form.errors,
        django_form.data,
        django_form.instance,
    )

    # "Saving" the form with commit=False should produce a record instance with
    # a data property that matches the cleaned form submission, but not
    # actually persist anything to the database.
    record_count = AppRecord.objects.count()
    unpersisted_record = django_form.save(commit=False)
    cleaned_record_data = django_form.cleaned_data
    assert {
        **unpersisted_record._data,
        "uuid": None,
        "app_form": unpersisted_record.app_form,
    } == cleaned_record_data
    assert AppRecord.objects.count() == record_count

    # Saving the form with commit=True should produce the same result as
    # commit=False, but actually persist the changes to the database.
    persisted_record = django_form.save(commit=True)
    assert {
        **persisted_record._data,
        "uuid": None,
        "app_form": persisted_record.app_form,
    } == cleaned_record_data
    assert AppRecord.objects.count() == record_count + 1

    # Recreating the form from the persisted record should produce a valid,
    # unchanged form. Calling save() on the form should noop.
    same_form = persisted_record.as_django_form(field_values)
    assert same_form.initial == {
        **persisted_record._data,
        "id": persisted_record.id,
        "uuid": persisted_record.uuid,
        "app_form": persisted_record.app_form,
    }
    assert same_form.is_valid(), same_form.errors
    assert not same_form.has_changed(), same_form.changed_data
    same_record = same_form.save()
    assert persisted_record is same_record
Пример #7
0
def test_fieldset() -> None:
    """Ensure that Django fieldsets can be produced for a Form."""
    form = FormFactory(label="Fieldsets Test")

    first_name_field = FieldFactory(
        form=form,
        label="First name",
        name="first_name",
        field_type=SingleLineTextField.name,
    )
    last_name_field = FieldFactory(
        form=form,
        label="Last name",
        name="last_name",
        field_type=SingleLineTextField.name,
    )

    birth_date_field = FieldFactory(
        form=form,
        label="Birth date",
        name="birth_date",
        field_type=DateTimeField.name,
    )

    avatar_field = FieldFactory(form=form,
                                label="Avatar",
                                name="avatar",
                                field_type=FileUploadField.name)

    bio_field = FieldFactory(form=form,
                             label="Bio",
                             name="bio",
                             field_type=MultiLineTextField.name)

    # A form with no fieldsets should return an empty list for as_django_fieldsets().
    assert form.as_django_fieldsets() == []

    # Create a fieldset for collecting basic info. It should have no header or description.
    basic_fieldset = form.fieldsets.create()

    # The fieldset should have a friendly name that includes its ID in __str__.
    assert str(basic_fieldset.pk) in str(basic_fieldset)

    # First and last name should appear on the same line within the fieldset.
    basic_fieldset.items.create(field=first_name_field,
                                vertical_order=0,
                                horizontal_order=0)
    basic_fieldset.items.create(field=last_name_field,
                                vertical_order=0,
                                horizontal_order=1)
    # Birth date should appear on its own line. Horizontal and vertical order
    # should act as a (weight as opposed to an index), so higher numbers in
    # either field should not result in gaps or empty elements in the rendered
    # fieldsets.
    basic_fieldset.items.create(field=birth_date_field,
                                vertical_order=10,
                                horizontal_order=10)

    # Create a fieldset for collecting profile info. It should have a header,
    # description, and CSS class names that will be split on spaces.
    profile_fieldset = form.fieldsets.create(name="Profile",
                                             description="Profile info",
                                             classes="profile collapse")

    profile_fieldset.items.create(field=avatar_field,
                                  vertical_order=0,
                                  horizontal_order=0)

    # Trying to put a field in the same slot as another field should raise an error.
    with pytest.raises(IntegrityError):
        profile_fieldset.items.create(field=bio_field,
                                      vertical_order=0,
                                      horizontal_order=0)

    fieldset_item = profile_fieldset.items.create(field=bio_field,
                                                  vertical_order=1,
                                                  horizontal_order=1)

    # The fieldset should have a friendly name that includes its ID in __str__.
    assert str(fieldset_item.pk) in str(fieldset_item)

    assert form.as_django_fieldsets() == [
        # The basic fieldset should come first and have no heading, classes, or description.
        (
            None,
            {
                "classes": (),
                "description":
                None,
                "fields": (
                    # The first and last name fields should be grouped together.
                    ("first_name", "last_name"),
                    # The birth date field should be on its own line,
                    # unwrapped, with no gaps or empty elements from the higher
                    # vertical and horizontal order numbers.
                    "birth_date",
                ),
            },
        ),
        # The profile fieldset should come last and have appropriate metadata values.
        (
            "Profile",
            {
                "classes": ("profile", "collapse"),
                "description": "Profile info",
                "fields": ("avatar", "bio"),
            },
        ),
    ]
Пример #8
0
def test_field_modifier() -> None:
    """Ensure that field modifiers can be created for a field.

    Tests validation logic for expression validity.
    """
    form = FormFactory()

    field_1 = FieldFactory(
        form=form,
        name="field_1",
        label="Test Field 1",
        field_type=SingleLineTextField.name,
    )

    field_2 = FieldFactory(
        form=form,
        name="field_2",
        label="Test Field 2",
        field_type=SingleLineTextField.name,
    )

    modifier = field_2.modifiers.create(attribute="required",
                                        expression="True")

    # If the modifier has no field, defer validation until save
    modifier.field = None
    modifier.clean()
    assert not modifier._validated
    modifier.field = field_2
    modifier.save()
    assert modifier._validated
    # Saving after validating should not result in a call to clean()
    modifier.save()
    assert modifier._validated

    # Ensure the field modifier has a friendly string representation
    assert str(modifier) == "App Field Modifier (required = True)"

    # Field modifiers that reference fields that don't exist should raise a
    # ValidationError.
    with pytest.raises(ValidationError) as ex:
        modifier.expression = "does_not_exist == 1"
        modifier.clean()
    error_message = str(ex.value.messages[0])

    # The validation message should tell the user what's wrong, and include the
    # name of the invalid variable as well as a list of valid ones.
    assert "no field with that name exists" in error_message
    assert "does_not_exist" in error_message
    assert ", ".join([field_1.name, field_2.name]) in error_message

    # Field modifiers that reference functions that don't exist should raise a
    # ValidationError.
    with pytest.raises(ValidationError) as ex:
        modifier.expression = "does_not_exist()"
        modifier.clean()
    error_message = str(ex.value.messages[0])

    # The validation message should tell the user what's wrong, and include the
    # name of the invalid function as well as a list of valid ones.
    assert "that function does not exist" in error_message
    assert "does_not_exist" in error_message
    assert ", ".join(FormEvaluator.FUNCTIONS.keys()) in error_message

    # Field modifiers that encounter other errors (like TypeErrors for
    # comparisons) should raise a ValidationError.
    with pytest.raises(ValidationError) as ex:
        modifier.expression = "'string' > 1"
        modifier.clean()
    error_message = str(ex.value.messages[0])

    # The validation message should tell the user what's wrong, and include the
    # exception message.
    assert "'>' not supported between instances of 'str' and 'int'" in str(
        ex.value.messages)
    assert "expression is invalid" in error_message