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.
Files changed (39) hide show
  1. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/CHANGELOG.md +16 -0
  2. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/PKG-INFO +1 -1
  3. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/RELEASE_NOTES.md +19 -6
  4. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/django_basic_form_builder.egg-info/PKG-INFO +1 -1
  5. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/admin.py +3 -1
  6. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/api/views.py +17 -2
  7. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/models.py +62 -7
  8. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_admin.py +26 -0
  9. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_api.py +34 -0
  10. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_models.py +122 -0
  11. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/pyproject.toml +1 -1
  12. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/CONFIGURATION_GUIDE.md +0 -0
  13. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/LICENSE +0 -0
  14. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/MANIFEST.in +0 -0
  15. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/QUICKSTART.md +0 -0
  16. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/README.md +0 -0
  17. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/django_basic_form_builder.egg-info/SOURCES.txt +0 -0
  18. {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
  19. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/django_basic_form_builder.egg-info/requires.txt +0 -0
  20. {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
  21. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/__init__.py +0 -0
  22. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/api/serializers.py +0 -0
  23. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/api/urls.py +0 -0
  24. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/apps.py +0 -0
  25. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/compat.py +0 -0
  26. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/migrations/0001_initial.py +0 -0
  27. {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
  28. {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
  29. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/migrations/__init__.py +0 -0
  30. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/schema_types.py +0 -0
  31. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/services/schema_builder.py +0 -0
  32. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/static/formbuilder/admin/css/formfield_admin.css +0 -0
  33. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/static/formbuilder/admin/js/formfield_admin.js +0 -0
  34. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/__init__.py +0 -0
  35. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/conftest.py +0 -0
  36. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_compat.py +0 -0
  37. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/tests/test_schema_builder.py +0 -0
  38. {django_basic_form_builder-0.1.3 → django_basic_form_builder-0.1.4}/formbuilder/views.py +0 -0
  39. {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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-basic-form-builder
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Reusable Django app for building JSON-driven forms via the admin
5
5
  Author-email: Göktürk Burak Köse <02macaw.others@icloud.com>
6
6
  License-Expression: MIT
@@ -1,10 +1,23 @@
1
- # Release Notes - v0.1.0
1
+ # Release Notes
2
2
 
3
- ## Overview
3
+ ## v0.1.4 - 2026-03-03
4
4
 
5
- This is the initial release of django-basic-form-builder, a reusable Django app that enables dynamic form creation through the Django admin with JSON schema generation for frontend consumption.
5
+ ### Overview
6
6
 
7
- ## What's Completed
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.0
148
- - **Release Date**: February 9, 2026
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+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-basic-form-builder
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Reusable Django app for building JSON-driven forms via the admin
5
5
  Author-email: Göktürk Burak Köse <02macaw.others@icloud.com>
6
6
  License-Expression: MIT
@@ -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)
@@ -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
- authentication_classes: list = []
16
- permission_classes: list = []
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
- min_date = config.get("minDate")
276
- max_date = config.get("maxDate")
277
- # Basic validation - actual date parsing would happen in frontend/submission
278
- if min_date and max_date:
279
- # Could add date comparison if needed
280
- pass
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,
@@ -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
@@ -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
@@ -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()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "django-basic-form-builder"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "Reusable Django app for building JSON-driven forms via the admin"
5
5
  requires-python = ">=3.12"
6
6
  readme = "README.md"