django-basic-form-builder 0.1.3__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/CHANGELOG.md +16 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/PKG-INFO +1 -1
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/RELEASE_NOTES.md +19 -6
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/django_basic_form_builder.egg-info/PKG-INFO +1 -1
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/admin.py +3 -1
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/api/views.py +17 -2
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/models.py +62 -7
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_admin.py +26 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_api.py +34 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_models.py +122 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/pyproject.toml +1 -1
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/CONFIGURATION_GUIDE.md +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/LICENSE +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/MANIFEST.in +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/QUICKSTART.md +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/README.md +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/django_basic_form_builder.egg-info/SOURCES.txt +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/django_basic_form_builder.egg-info/dependency_links.txt +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/django_basic_form_builder.egg-info/requires.txt +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/django_basic_form_builder.egg-info/top_level.txt +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/__init__.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/api/serializers.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/api/urls.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/apps.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/compat.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/migrations/0001_initial.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/migrations/0002_alter_formfield_field_type_fieldoption.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/migrations/0003_formfield_question_alter_formfield_label.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/migrations/__init__.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/schema_types.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/services/schema_builder.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/static/formbuilder/admin/css/formfield_admin.css +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/static/formbuilder/admin/js/formfield_admin.js +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/__init__.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/conftest.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_compat.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_schema_builder.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/views.py +0 -0
- {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/setup.cfg +0 -0
|
@@ -5,6 +5,22 @@ All notable changes to django-basic-form-builder will be documented in this file
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.4] - 2026-03-03
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **High**: Enforced `has_view_permission()` model-level checks within the `preview_view` endpoint to prevent low-privileged staff accounts from viewing draft form definitions.
|
|
13
|
+
- **Medium**: Removed hardcoded `authentication_classes = []` and `permission_classes = []` on the public API endpoint (`FormSchemaView`). The API now respects the host-project's defaults unless `FORMBUILDER_API_ANONYMOUS = True` is set.
|
|
14
|
+
- **Medium**: Tightened permissive schema configuration validation to prevent misinterpretation or Denial-of-Service attacks in downstream consumers.
|
|
15
|
+
- Rejected booleans for integer/numeric config fields since `bool` is a subclass of `int`.
|
|
16
|
+
- Enforced a 500-character max length on regex patterns and validated compilation with `re.compile()`.
|
|
17
|
+
- Added full ISO parsing for `minDate`/`maxDate` and strict format whitelisting for dates.
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
- Added a warning note about `bulk_create` caveats in relation to single-default invariants within the `FieldOption` model.
|
|
22
|
+
- Further clarified the test site `settings.py` usage constraints.
|
|
23
|
+
|
|
8
24
|
## [0.1.3] - 2026-02-09
|
|
9
25
|
|
|
10
26
|
### Changed
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
# Release Notes
|
|
1
|
+
# Release Notes
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## v0.1.4 - 2026-03-03
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Overview
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Version 0.1.4 is a critical security-focused patch release addressing vulnerabilities identified in the 2026-03-03 security review. It enforces strict model-level permissions for admin preview endpoints, respects application-level API configurations by default, and implements comprehensive schema type validation and compilation to secure applications against arbitrary or malformed input payloads.
|
|
8
|
+
|
|
9
|
+
### What's Changed ✅
|
|
10
|
+
|
|
11
|
+
- Enforced permission checks (`has_view_permission()`) on the admin `preview_view` endpoint.
|
|
12
|
+
- Handled API endpoint access dynamically using DRF's default configurations. Restored via `FORMBUILDER_API_ANONYMOUS = True` if required.
|
|
13
|
+
- Implemented tight restriction on Boolean conversions for integer/numeric types within `models.py` configuration validations.
|
|
14
|
+
- Explicit Regex compilation with `re.compile()` and a 500-character upper ceiling constraint to prevent ReDoS payloads.
|
|
15
|
+
- Added strict format options and verified parsing logic for `minDate`/`maxDate` in configurations.
|
|
16
|
+
- Improved documentation with `FieldOption` model warnings about bulk operations and updated the `formbuilder_test_site` environments.
|
|
17
|
+
|
|
18
|
+
## v0.1.0 - Initial Release
|
|
19
|
+
|
|
20
|
+
### Overview
|
|
8
21
|
|
|
9
22
|
### Core Features
|
|
10
23
|
|
|
@@ -144,8 +157,8 @@ For issues, questions, or contributions, please refer to:
|
|
|
144
157
|
|
|
145
158
|
## Version Information
|
|
146
159
|
|
|
147
|
-
- **Version**: 0.1.
|
|
148
|
-
- **Release Date**:
|
|
160
|
+
- **Latest Version**: 0.1.4
|
|
161
|
+
- **Release Date**: March 3, 2026
|
|
149
162
|
- **Django**: 5.1+
|
|
150
163
|
- **DRF**: 3.15+
|
|
151
164
|
- **Python**: 3.12+
|
|
@@ -4,7 +4,7 @@ import contextlib
|
|
|
4
4
|
|
|
5
5
|
from django import forms
|
|
6
6
|
from django.contrib import admin
|
|
7
|
-
from django.core.exceptions import ValidationError
|
|
7
|
+
from django.core.exceptions import PermissionDenied, ValidationError
|
|
8
8
|
from django.http import JsonResponse
|
|
9
9
|
from django.shortcuts import get_object_or_404
|
|
10
10
|
from django.urls import path, reverse
|
|
@@ -298,6 +298,8 @@ class CustomFormAdmin(admin.ModelAdmin):
|
|
|
298
298
|
|
|
299
299
|
def preview_view(self, request, pk, *args, **kwargs):
|
|
300
300
|
custom_form = get_object_or_404(CustomForm, pk=pk)
|
|
301
|
+
if not self.has_view_permission(request, custom_form):
|
|
302
|
+
raise PermissionDenied
|
|
301
303
|
if not custom_form.json_schema:
|
|
302
304
|
custom_form.generate_schema(commit=True)
|
|
303
305
|
return JsonResponse(custom_form.json_schema)
|
{django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/api/views.py
RENAMED
|
@@ -5,6 +5,7 @@ from django.http import Http404
|
|
|
5
5
|
from django.shortcuts import get_object_or_404
|
|
6
6
|
from drf_spectacular.utils import extend_schema
|
|
7
7
|
from rest_framework.response import Response
|
|
8
|
+
from rest_framework.settings import api_settings
|
|
8
9
|
from rest_framework.views import APIView
|
|
9
10
|
|
|
10
11
|
from ..models import CustomForm
|
|
@@ -12,8 +13,20 @@ from .serializers import FormSchemaSerializer
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class FormSchemaView(APIView):
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
# By default, authentication and permission classes are inherited from
|
|
17
|
+
# DRF defaults (i.e. the host project's REST_FRAMEWORK settings).
|
|
18
|
+
# Set FORMBUILDER_API_ANONYMOUS = True in Django settings to allow
|
|
19
|
+
# unauthenticated access (restores pre-0.1.4 behavior).
|
|
20
|
+
|
|
21
|
+
def get_authenticators(self):
|
|
22
|
+
if getattr(settings, "FORMBUILDER_API_ANONYMOUS", False):
|
|
23
|
+
return []
|
|
24
|
+
return [auth() for auth in api_settings.DEFAULT_AUTHENTICATION_CLASSES]
|
|
25
|
+
|
|
26
|
+
def get_permissions(self):
|
|
27
|
+
if getattr(settings, "FORMBUILDER_API_ANONYMOUS", False):
|
|
28
|
+
return []
|
|
29
|
+
return [permission() for permission in api_settings.DEFAULT_PERMISSION_CLASSES]
|
|
17
30
|
|
|
18
31
|
@extend_schema(responses=FormSchemaSerializer)
|
|
19
32
|
def get(self, request, slug: str, *args, **kwargs):
|
|
@@ -27,3 +40,5 @@ class FormSchemaView(APIView):
|
|
|
27
40
|
)
|
|
28
41
|
serializer = FormSchemaSerializer(instance=custom_form.json_schema)
|
|
29
42
|
return Response(serializer.data)
|
|
43
|
+
|
|
44
|
+
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
4
|
+
import re
|
|
3
5
|
from collections.abc import Iterable
|
|
4
6
|
from numbers import Number
|
|
5
7
|
from typing import Any
|
|
@@ -195,6 +197,14 @@ class FormField(models.Model):
|
|
|
195
197
|
for key, allowed_types in self.FIELD_CONFIG_SCHEMA.get(self.field_type, {}).items():
|
|
196
198
|
if key not in config:
|
|
197
199
|
continue
|
|
200
|
+
# Reject bool for non-bool fields (bool is a subclass of int)
|
|
201
|
+
if isinstance(config[key], bool) and bool not in allowed_types:
|
|
202
|
+
raise ValidationError(
|
|
203
|
+
{
|
|
204
|
+
"config": _("%(key)s must not be a boolean.")
|
|
205
|
+
% {"key": key}
|
|
206
|
+
}
|
|
207
|
+
)
|
|
198
208
|
if not isinstance(config[key], tuple(allowed_types)):
|
|
199
209
|
raise ValidationError(
|
|
200
210
|
{
|
|
@@ -210,6 +220,7 @@ class FormField(models.Model):
|
|
|
210
220
|
config = self.config or {}
|
|
211
221
|
if self.field_type == self.FieldType.TEXT:
|
|
212
222
|
self._ensure_min_max_relationship(config, "minLength", "maxLength")
|
|
223
|
+
self._validate_regex_pattern(config)
|
|
213
224
|
elif self.field_type == self.FieldType.NUMBER:
|
|
214
225
|
self._validate_numeric_config(config)
|
|
215
226
|
elif self.field_type == self.FieldType.TEXTAREA:
|
|
@@ -271,13 +282,50 @@ class FormField(models.Model):
|
|
|
271
282
|
{"config": _("Rating style must be one of: stars, numeric, emoji.")}
|
|
272
283
|
)
|
|
273
284
|
|
|
285
|
+
_MAX_REGEX_LENGTH = 500
|
|
286
|
+
|
|
287
|
+
def _validate_regex_pattern(self, config: dict[str, Any]) -> None:
|
|
288
|
+
pattern = config.get("pattern")
|
|
289
|
+
if not pattern:
|
|
290
|
+
return
|
|
291
|
+
if len(pattern) > self._MAX_REGEX_LENGTH:
|
|
292
|
+
raise ValidationError(
|
|
293
|
+
{"config": _("pattern is too long (max %(max)d characters).") % {"max": self._MAX_REGEX_LENGTH}}
|
|
294
|
+
)
|
|
295
|
+
try:
|
|
296
|
+
re.compile(pattern)
|
|
297
|
+
except re.error as exc:
|
|
298
|
+
raise ValidationError(
|
|
299
|
+
{"config": _("pattern is not a valid regex: %(err)s") % {"err": str(exc)}}
|
|
300
|
+
) from exc
|
|
301
|
+
|
|
302
|
+
ALLOWED_DATE_FORMATS = frozenset({"YYYY-MM-DD", "DD/MM/YYYY", "MM/DD/YYYY", "DD.MM.YYYY"})
|
|
303
|
+
|
|
274
304
|
def _validate_date_config(self, config: dict[str, Any]) -> None:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
305
|
+
for key in ("minDate", "maxDate"):
|
|
306
|
+
value = config.get(key)
|
|
307
|
+
if value:
|
|
308
|
+
try:
|
|
309
|
+
datetime.date.fromisoformat(value)
|
|
310
|
+
except (ValueError, TypeError) as exc:
|
|
311
|
+
raise ValidationError(
|
|
312
|
+
{"config": _("%(key)s must be a valid ISO date (YYYY-MM-DD).") % {"key": key}}
|
|
313
|
+
) from exc
|
|
314
|
+
|
|
315
|
+
fmt = config.get("format")
|
|
316
|
+
if fmt and fmt not in self.ALLOWED_DATE_FORMATS:
|
|
317
|
+
raise ValidationError(
|
|
318
|
+
{
|
|
319
|
+
"config": _("format must be one of: %(formats)s")
|
|
320
|
+
% {"formats": ", ".join(sorted(self.ALLOWED_DATE_FORMATS))}
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
min_date_str = config.get("minDate")
|
|
325
|
+
max_date_str = config.get("maxDate")
|
|
326
|
+
if min_date_str and max_date_str:
|
|
327
|
+
if datetime.date.fromisoformat(min_date_str) > datetime.date.fromisoformat(max_date_str):
|
|
328
|
+
raise ValidationError({"config": _("minDate cannot be after maxDate.")})
|
|
281
329
|
|
|
282
330
|
def _validate_dropdown_config(self, config: dict[str, Any]) -> None:
|
|
283
331
|
"""Legacy method - kept for backward compatibility with old config-based options"""
|
|
@@ -302,7 +350,14 @@ class FormField(models.Model):
|
|
|
302
350
|
|
|
303
351
|
|
|
304
352
|
class FieldOption(models.Model):
|
|
305
|
-
"""Options for dropdown, radio, and checkbox fields
|
|
353
|
+
"""Options for dropdown, radio, and checkbox fields.
|
|
354
|
+
|
|
355
|
+
Note: The single-default invariant for radio/dropdown fields is enforced in
|
|
356
|
+
``clean()``, which is called by ``save()`` via ``full_clean()``. Code paths
|
|
357
|
+
that bypass ``save()`` (e.g. ``bulk_create``, raw SQL, or ``update()``)
|
|
358
|
+
will NOT enforce this constraint. Always use ``save()`` or call
|
|
359
|
+
``full_clean()`` explicitly when creating or updating options.
|
|
360
|
+
"""
|
|
306
361
|
|
|
307
362
|
field = models.ForeignKey(
|
|
308
363
|
FormField,
|
{django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_admin.py
RENAMED
|
@@ -52,3 +52,29 @@ def test_preview_view_returns_json(custom_form: CustomForm, db):
|
|
|
52
52
|
|
|
53
53
|
assert response.status_code == 200
|
|
54
54
|
assert response.json()["fields"][0]["id"] == "email"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_preview_view_forbidden_without_view_permission(custom_form: CustomForm, db):
|
|
58
|
+
"""Staff user without view_customform permission should get 403."""
|
|
59
|
+
FormField.objects.create(
|
|
60
|
+
custom_form=custom_form,
|
|
61
|
+
label="Email",
|
|
62
|
+
slug="email",
|
|
63
|
+
field_type=FormField.FieldType.TEXT,
|
|
64
|
+
position=1,
|
|
65
|
+
required=True,
|
|
66
|
+
config={"inputMode": "email"},
|
|
67
|
+
)
|
|
68
|
+
custom_form.refresh_from_db()
|
|
69
|
+
|
|
70
|
+
user_model = auth.get_user_model()
|
|
71
|
+
user_model.objects.create_user(
|
|
72
|
+
username="staffuser", email="staff@example.com", password="pass", is_staff=True
|
|
73
|
+
)
|
|
74
|
+
client = Client()
|
|
75
|
+
assert client.login(username="staffuser", password="pass")
|
|
76
|
+
|
|
77
|
+
url = reverse("admin:formbuilder_customform_preview", args=[custom_form.pk])
|
|
78
|
+
response = client.get(url)
|
|
79
|
+
|
|
80
|
+
assert response.status_code == 403
|
{django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_api.py
RENAMED
|
@@ -63,3 +63,37 @@ def test_api_filters_unpublished_forms(custom_form: CustomForm):
|
|
|
63
63
|
response = client.get("/api/forms/contact-form/")
|
|
64
64
|
|
|
65
65
|
assert response.status_code == 404
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@override_settings(
|
|
69
|
+
FORMBUILDER_API_ANONYMOUS=False,
|
|
70
|
+
REST_FRAMEWORK={
|
|
71
|
+
"DEFAULT_PERMISSION_CLASSES": [
|
|
72
|
+
"rest_framework.permissions.IsAuthenticated",
|
|
73
|
+
],
|
|
74
|
+
"DEFAULT_RENDERER_CLASSES": [
|
|
75
|
+
"rest_framework.renderers.JSONRenderer",
|
|
76
|
+
],
|
|
77
|
+
"DEFAULT_PARSER_CLASSES": [
|
|
78
|
+
"rest_framework.parsers.JSONParser",
|
|
79
|
+
],
|
|
80
|
+
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
def test_api_respects_drf_defaults_when_not_anonymous(custom_form: CustomForm):
|
|
84
|
+
"""When FORMBUILDER_API_ANONYMOUS is False, DRF defaults should apply."""
|
|
85
|
+
FormField.objects.create(
|
|
86
|
+
custom_form=custom_form,
|
|
87
|
+
label="Email",
|
|
88
|
+
slug="email",
|
|
89
|
+
field_type=FormField.FieldType.TEXT,
|
|
90
|
+
position=1,
|
|
91
|
+
required=True,
|
|
92
|
+
config={"inputMode": "email"},
|
|
93
|
+
)
|
|
94
|
+
_publish_form(custom_form)
|
|
95
|
+
|
|
96
|
+
client = APIClient()
|
|
97
|
+
response = client.get("/api/forms/contact-form/")
|
|
98
|
+
|
|
99
|
+
assert response.status_code == 403
|
{django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_models.py
RENAMED
|
@@ -258,3 +258,125 @@ def test_dropdown_cannot_have_multiple_defaults(custom_form: CustomForm):
|
|
|
258
258
|
|
|
259
259
|
assert "is_default" in exc_info.value.error_dict
|
|
260
260
|
assert "Only one default option is allowed for this field." in str(exc_info.value)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# -- Validation hardening regression tests (Finding 3) --
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@pytest.mark.parametrize(
|
|
267
|
+
"field_type, config",
|
|
268
|
+
[
|
|
269
|
+
(FormField.FieldType.TEXT, {"minLength": True}),
|
|
270
|
+
(FormField.FieldType.TEXT, {"maxLength": True}),
|
|
271
|
+
(FormField.FieldType.TEXTAREA, {"rows": True}),
|
|
272
|
+
(FormField.FieldType.NUMBER, {"step": True}),
|
|
273
|
+
(FormField.FieldType.CHECKBOX, {"minSelections": True}),
|
|
274
|
+
(FormField.FieldType.RATING, {"scale": True}),
|
|
275
|
+
],
|
|
276
|
+
)
|
|
277
|
+
def test_boolean_rejected_for_numeric_fields(custom_form: CustomForm, field_type, config):
|
|
278
|
+
"""bool values must not pass validation for integer/numeric config keys."""
|
|
279
|
+
field = FormField(
|
|
280
|
+
custom_form=custom_form,
|
|
281
|
+
label="Test",
|
|
282
|
+
slug="test-bool",
|
|
283
|
+
field_type=field_type,
|
|
284
|
+
position=1,
|
|
285
|
+
config=config,
|
|
286
|
+
)
|
|
287
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
288
|
+
field.full_clean()
|
|
289
|
+
assert "must not be a boolean" in str(exc_info.value)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def test_invalid_regex_pattern_rejected(custom_form: CustomForm):
|
|
293
|
+
"""Malformed regex patterns must be rejected."""
|
|
294
|
+
field = FormField(
|
|
295
|
+
custom_form=custom_form,
|
|
296
|
+
label="Name",
|
|
297
|
+
slug="name",
|
|
298
|
+
field_type=FormField.FieldType.TEXT,
|
|
299
|
+
position=1,
|
|
300
|
+
config={"pattern": "([a-z+"},
|
|
301
|
+
)
|
|
302
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
303
|
+
field.full_clean()
|
|
304
|
+
assert "not a valid regex" in str(exc_info.value)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_regex_pattern_too_long_rejected(custom_form: CustomForm):
|
|
308
|
+
"""Regex patterns exceeding max length must be rejected."""
|
|
309
|
+
field = FormField(
|
|
310
|
+
custom_form=custom_form,
|
|
311
|
+
label="Name",
|
|
312
|
+
slug="name",
|
|
313
|
+
field_type=FormField.FieldType.TEXT,
|
|
314
|
+
position=1,
|
|
315
|
+
config={"pattern": "a" * 501},
|
|
316
|
+
)
|
|
317
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
318
|
+
field.full_clean()
|
|
319
|
+
assert "too long" in str(exc_info.value)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_valid_regex_pattern_accepted(custom_form: CustomForm):
|
|
323
|
+
"""A valid regex pattern should pass validation."""
|
|
324
|
+
field = FormField(
|
|
325
|
+
custom_form=custom_form,
|
|
326
|
+
label="Name",
|
|
327
|
+
slug="name",
|
|
328
|
+
field_type=FormField.FieldType.TEXT,
|
|
329
|
+
position=1,
|
|
330
|
+
config={"pattern": "^[a-zA-Z]+$"},
|
|
331
|
+
)
|
|
332
|
+
field.full_clean()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@pytest.mark.parametrize(
|
|
336
|
+
"config",
|
|
337
|
+
[
|
|
338
|
+
{"minDate": "not-a-date"},
|
|
339
|
+
{"maxDate": "2020-99-99"},
|
|
340
|
+
{"format": "%Q"},
|
|
341
|
+
],
|
|
342
|
+
)
|
|
343
|
+
def test_invalid_date_config_rejected(custom_form: CustomForm, config):
|
|
344
|
+
"""Invalid date strings and unsupported formats must be rejected."""
|
|
345
|
+
field = FormField(
|
|
346
|
+
custom_form=custom_form,
|
|
347
|
+
label="Birthday",
|
|
348
|
+
slug="birthday",
|
|
349
|
+
field_type=FormField.FieldType.DATE,
|
|
350
|
+
position=1,
|
|
351
|
+
config=config,
|
|
352
|
+
)
|
|
353
|
+
with pytest.raises(ValidationError):
|
|
354
|
+
field.full_clean()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_min_date_after_max_date_rejected(custom_form: CustomForm):
|
|
358
|
+
"""minDate after maxDate must be rejected."""
|
|
359
|
+
field = FormField(
|
|
360
|
+
custom_form=custom_form,
|
|
361
|
+
label="Birthday",
|
|
362
|
+
slug="birthday",
|
|
363
|
+
field_type=FormField.FieldType.DATE,
|
|
364
|
+
position=1,
|
|
365
|
+
config={"minDate": "2025-12-31", "maxDate": "2025-01-01"},
|
|
366
|
+
)
|
|
367
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
368
|
+
field.full_clean()
|
|
369
|
+
assert "minDate cannot be after maxDate" in str(exc_info.value)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_valid_date_config_accepted(custom_form: CustomForm):
|
|
373
|
+
"""Valid date config should pass validation."""
|
|
374
|
+
field = FormField(
|
|
375
|
+
custom_form=custom_form,
|
|
376
|
+
label="Birthday",
|
|
377
|
+
slug="birthday",
|
|
378
|
+
field_type=FormField.FieldType.DATE,
|
|
379
|
+
position=1,
|
|
380
|
+
config={"minDate": "2025-01-01", "maxDate": "2025-12-31", "format": "YYYY-MM-DD"},
|
|
381
|
+
)
|
|
382
|
+
field.full_clean()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/api/serializers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/schema_types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/__init__.py
RENAMED
|
File without changes
|
{django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/conftest.py
RENAMED
|
File without changes
|
{django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_compat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|