django-forms-workflows 0.33.2__tar.gz → 0.34.0__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 (148) hide show
  1. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/PKG-INFO +1 -1
  2. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/forms.py +93 -2
  3. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/static/django_forms_workflows/js/form-enhancements.js +29 -0
  4. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/approve.html +206 -39
  5. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/submission_detail.html +68 -1
  6. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/submission_pdf.html +9 -0
  7. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/views.py +66 -5
  8. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/pyproject.toml +1 -1
  9. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/CHANGELOG.md +0 -0
  10. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/LICENSE +0 -0
  11. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/README.md +0 -0
  12. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/__init__.py +0 -0
  13. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/admin.py +0 -0
  14. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/apps.py +0 -0
  15. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/conditions.py +0 -0
  16. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/context_processors.py +0 -0
  17. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/data_sources/__init__.py +0 -0
  18. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/data_sources/base.py +0 -0
  19. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/data_sources/database_source.py +0 -0
  20. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/data_sources/ldap_source.py +0 -0
  21. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/data_sources/user_source.py +0 -0
  22. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/diff_views.py +0 -0
  23. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/email_backends/__init__.py +0 -0
  24. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/email_backends/gmail_api.py +0 -0
  25. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/form_builder_urls.py +0 -0
  26. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/form_builder_views.py +0 -0
  27. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/handlers/__init__.py +0 -0
  28. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/handlers/api_handler.py +0 -0
  29. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/handlers/base.py +0 -0
  30. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/handlers/database_handler.py +0 -0
  31. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/handlers/email_handler.py +0 -0
  32. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/handlers/executor.py +0 -0
  33. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/handlers/file_handler.py +0 -0
  34. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/handlers/ldap_handler.py +0 -0
  35. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/ldap_backend.py +0 -0
  36. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/management/commands/create_default_templates.py +0 -0
  37. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/management/commands/pull_forms.py +0 -0
  38. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/management/commands/push_forms.py +0 -0
  39. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/management/commands/seed_farm_demo.py +0 -0
  40. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/management/commands/seed_pcn_form.py +0 -0
  41. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/management/commands/seed_prefill_sources.py +0 -0
  42. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/management/commands/sync_ldap_profiles.py +0 -0
  43. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/management/commands/test_db_connection.py +0 -0
  44. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0001_initial.py +0 -0
  45. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0002_prefillsource_alter_formfield_prefill_source_and_more.py +0 -0
  46. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0003_postsubmissionaction.py +0 -0
  47. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0004_formtemplate.py +0 -0
  48. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0005_advanced_conditional_logic.py +0 -0
  49. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0006_workflowdefinition_visual_workflow_data_and_more.py +0 -0
  50. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0007_add_userprofile_ldap_enhancements.py +0 -0
  51. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0008_add_file_workflow_models.py +0 -0
  52. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0009_add_formfield_readonly.py +0 -0
  53. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0010_add_prefillsource_template_fields.py +0 -0
  54. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0011_postsubmissionaction_email_body_template_and_more.py +0 -0
  55. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0012_add_approval_step_fields.py +0 -0
  56. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0013_add_database_query_key.py +0 -0
  57. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0014_add_form_category.py +0 -0
  58. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0015_add_workflowstage_staged_approvals.py +0 -0
  59. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0016_add_pdf_generation.py +0 -0
  60. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0017_add_notification_batching.py +0 -0
  61. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0018_add_form_category_parent.py +0 -0
  62. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0019_add_ldap_group_profile.py +0 -0
  63. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0020_add_bulk_export_field.py +0 -0
  64. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0021_alter_pendingnotification_scheduled_for_and_more.py +0 -0
  65. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0022_add_bulk_pdf_export.py +0 -0
  66. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0023_add_performance_indexes.py +0 -0
  67. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0024_add_allow_resubmit.py +0 -0
  68. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0025_workflowstage_approve_label.py +0 -0
  69. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0026_alter_formfield_width.py +0 -0
  70. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0027_formsubmission_form_data_gin.py +0 -0
  71. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0028_workflowstagegroupconfig.py +0 -0
  72. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0029_formfield_workflow_stage_fk_drop_groupconfig.py +0 -0
  73. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0030_formfield_phone_type.py +0 -0
  74. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0031_sub_workflows.py +0 -0
  75. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0032_approved_pending_detached_sub_workflow.py +0 -0
  76. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0033_add_reject_parent_to_subworkflowdefinition.py +0 -0
  77. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0034_add_notificationlog.py +0 -0
  78. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0035_migrate_prefill_source_strings.py +0 -0
  79. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0036_add_workflowstage_auto_created.py +0 -0
  80. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0037_migrate_flat_workflows_to_staged.py +0 -0
  81. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0038_remove_legacy_prefill_source_and_approval_step.py +0 -0
  82. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0039_update_help_text.py +0 -0
  83. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0040_migrate_deprecated_features.py +0 -0
  84. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0041_remove_deprecated_fields.py +0 -0
  85. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0042_add_sub_workflow_section_label.py +0 -0
  86. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0043_fix_form_data_decimal_encoding.py +0 -0
  87. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0044_remove_subworkflowinstance_label.py +0 -0
  88. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0045_stageapprovalgroup_workflow_name_label.py +0 -0
  89. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0046_workflow_definition_one_to_many.py +0 -0
  90. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0047_add_hide_approval_history.py +0 -0
  91. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0048_workflowdefinition_trigger_conditions_and_more.py +0 -0
  92. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0049_formfield_multifile_type.py +0 -0
  93. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0050_workflowdefinition_collapse_parallel_stages.py +0 -0
  94. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0051_formdefinition_is_listed.py +0 -0
  95. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0052_dynamic_assignee_and_form_field_notifications.py +0 -0
  96. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0053_stageformfieldnotification_static_emails.py +0 -0
  97. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0054_rename_approved_pending_to_pending_approval.py +0 -0
  98. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0055_add_send_back.py +0 -0
  99. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0056_calculated_formula_spreadsheet.py +0 -0
  100. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/0057_add_currency_field_type.py +0 -0
  101. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/migrations/__init__.py +0 -0
  102. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/models.py +0 -0
  103. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/signals.py +0 -0
  104. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/sso_backends.py +0 -0
  105. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/sso_urls.py +0 -0
  106. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/sso_views.py +0 -0
  107. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/static/django_forms_workflows/js/form-builder.js +0 -0
  108. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/static/django_forms_workflows/js/workflow-builder.js +0 -0
  109. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/sync_api.py +0 -0
  110. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/sync_views.py +0 -0
  111. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/tasks.py +0 -0
  112. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/admin/django_forms_workflows/_sync_diff.html +0 -0
  113. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/admin/django_forms_workflows/diff_forms.html +0 -0
  114. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/admin/django_forms_workflows/form_builder.html +0 -0
  115. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/admin/django_forms_workflows/formdef_change_form.html +0 -0
  116. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/admin/django_forms_workflows/formdefinition/change_list.html +0 -0
  117. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_import.html +0 -0
  118. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_pull.html +0 -0
  119. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_push.html +0 -0
  120. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/admin/django_forms_workflows/workflow_builder.html +0 -0
  121. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/_category_node.html +0 -0
  122. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/approval_inbox.html +0 -0
  123. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/base.html +0 -0
  124. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/completed_approvals.html +0 -0
  125. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/form_categories.html +0 -0
  126. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/form_list.html +0 -0
  127. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/form_submit.html +0 -0
  128. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/my_submissions.html +0 -0
  129. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/resubmit_confirm.html +0 -0
  130. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/sso/login.html +0 -0
  131. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/sub_workflow_detail.html +0 -0
  132. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/submission_bulk_pdf.html +0 -0
  133. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/django_forms_workflows/withdraw_confirm.html +0 -0
  134. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/emails/approval_notification.html +0 -0
  135. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/emails/approval_reminder.html +0 -0
  136. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/emails/approval_request.html +0 -0
  137. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/emails/email_styles.html +0 -0
  138. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/emails/escalation_notification.html +0 -0
  139. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/emails/notification_digest.html +0 -0
  140. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/emails/rejection_notification.html +0 -0
  141. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/emails/submission_notification.html +0 -0
  142. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templates/registration/login.html +0 -0
  143. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templatetags/__init__.py +0 -0
  144. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/templatetags/forms_workflows_tags.py +0 -0
  145. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/urls.py +0 -0
  146. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/utils.py +0 -0
  147. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/workflow_builder_views.py +0 -0
  148. {django_forms_workflows-0.33.2 → django_forms_workflows-0.34.0}/django_forms_workflows/workflow_engine.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-forms-workflows
3
- Version: 0.33.2
3
+ Version: 0.34.0
4
4
  Summary: Enterprise-grade, database-driven form builder with approval workflows and external data integration
5
5
  License: LGPL-3.0-only
6
6
  License-File: LICENSE
@@ -13,7 +13,9 @@ from datetime import date, datetime
13
13
  from crispy_forms.helper import FormHelper
14
14
  from crispy_forms.layout import HTML, Column, Div, Field, Layout, Row, Submit
15
15
  from django import forms
16
- from django.core.validators import RegexValidator
16
+ from django.core.exceptions import ValidationError
17
+ from django.core.validators import FileExtensionValidator, RegexValidator
18
+ from django.utils.deconstruct import deconstructible
17
19
 
18
20
  logger = logging.getLogger(__name__)
19
21
 
@@ -38,6 +40,52 @@ def _evaluate_formula(formula: str, data: dict) -> str:
38
40
  )
39
41
 
40
42
 
43
+ # ---------------------------------------------------------------------------
44
+ # File upload validators
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ @deconstructible
49
+ class MaxFileSizeValidator:
50
+ """Validate that a file does not exceed a given size in megabytes."""
51
+
52
+ message = "File size must not exceed %(max_size)s MB."
53
+ code = "file_too_large"
54
+
55
+ def __init__(self, max_size_mb):
56
+ self.max_size_mb = max_size_mb
57
+
58
+ def __call__(self, value):
59
+ if hasattr(value, "size") and value.size > self.max_size_mb * 1024 * 1024:
60
+ raise ValidationError(
61
+ self.message,
62
+ code=self.code,
63
+ params={"max_size": self.max_size_mb},
64
+ )
65
+
66
+ def __eq__(self, other):
67
+ return (
68
+ isinstance(other, MaxFileSizeValidator)
69
+ and self.max_size_mb == other.max_size_mb
70
+ )
71
+
72
+
73
+ def _build_file_validators(field_def):
74
+ """Return a list of validators for file fields based on field definition."""
75
+ validators = []
76
+ if field_def.allowed_extensions:
77
+ exts = [
78
+ e.strip().lower()
79
+ for e in field_def.allowed_extensions.split(",")
80
+ if e.strip()
81
+ ]
82
+ if exts:
83
+ validators.append(FileExtensionValidator(allowed_extensions=exts))
84
+ if field_def.max_file_size_mb:
85
+ validators.append(MaxFileSizeValidator(field_def.max_file_size_mb))
86
+ return validators
87
+
88
+
41
89
  # ---------------------------------------------------------------------------
42
90
  # Multi-file upload support
43
91
  # ---------------------------------------------------------------------------
@@ -373,7 +421,22 @@ class DynamicForm(forms.Form):
373
421
  )
374
422
 
375
423
  elif field_def.field_type == "file":
376
- self.fields[field_def.field_name] = forms.FileField(**field_args)
424
+ file_validators = _build_file_validators(field_def)
425
+ accept_attrs = {}
426
+ if field_def.allowed_extensions:
427
+ exts = ",".join(
428
+ f".{e.strip()}"
429
+ for e in field_def.allowed_extensions.split(",")
430
+ if e.strip()
431
+ )
432
+ accept_attrs["accept"] = exts
433
+ self.fields[field_def.field_name] = forms.FileField(
434
+ validators=file_validators,
435
+ widget=forms.ClearableFileInput(attrs=accept_attrs)
436
+ if accept_attrs
437
+ else forms.ClearableFileInput(),
438
+ **field_args,
439
+ )
377
440
 
378
441
  elif field_def.field_type == "multifile":
379
442
  self.fields[field_def.field_name] = MultipleFileField(
@@ -679,6 +742,32 @@ class DynamicForm(forms.Form):
679
742
  }
680
743
  )
681
744
 
745
+ # File type / size validation rules
746
+ if field.field_type in ("file", "multifile") and field.allowed_extensions:
747
+ exts = [
748
+ e.strip().lower()
749
+ for e in field.allowed_extensions.split(",")
750
+ if e.strip()
751
+ ]
752
+ if exts:
753
+ readable = ", ".join(f".{e}" for e in exts)
754
+ validation_rules.append(
755
+ {
756
+ "type": "file_type",
757
+ "value": ",".join(exts),
758
+ "message": f"Allowed file types: {readable}",
759
+ }
760
+ )
761
+
762
+ if field.field_type in ("file", "multifile") and field.max_file_size_mb:
763
+ validation_rules.append(
764
+ {
765
+ "type": "file_size",
766
+ "value": field.max_file_size_mb,
767
+ "message": f"File size must not exceed {field.max_file_size_mb} MB",
768
+ }
769
+ )
770
+
682
771
  # Custom validation rules from field config
683
772
  if hasattr(field, "validation_rules") and field.validation_rules:
684
773
  if isinstance(field.validation_rules, str):
@@ -940,6 +1029,7 @@ class ApprovalStepForm(forms.Form):
940
1029
  )
941
1030
 
942
1031
  elif field_def.field_type == "file":
1032
+ file_validators = _build_file_validators(field_def)
943
1033
  accept_attrs = {}
944
1034
  if field_def.allowed_extensions:
945
1035
  exts = ",".join(
@@ -952,6 +1042,7 @@ class ApprovalStepForm(forms.Form):
952
1042
  required=field_args.get("required", False),
953
1043
  label=field_def.field_label,
954
1044
  help_text=field_def.help_text,
1045
+ validators=file_validators,
955
1046
  widget=forms.ClearableFileInput(attrs=accept_attrs),
956
1047
  )
957
1048
 
@@ -115,6 +115,8 @@ class FormEnhancements {
115
115
  } else if (field.type === 'radio') {
116
116
  const checked = this.form.querySelector(`input[name="${field.name}"]:checked`);
117
117
  return checked ? checked.value : null;
118
+ } else if (field.type === 'file') {
119
+ return field.files && field.files.length > 0 ? field.files : null;
118
120
  } else if (field.multiple) {
119
121
  const values = Array.from(field.selectedOptions).map(opt => opt.value);
120
122
  return values.length > 0 ? values : null;
@@ -444,6 +446,9 @@ class FormEnhancements {
444
446
  validateRule(value, rule) {
445
447
  switch (rule.type) {
446
448
  case 'required':
449
+ if (value instanceof FileList) {
450
+ return value.length > 0;
451
+ }
447
452
  return value !== null && value !== undefined && value !== '' &&
448
453
  (!Array.isArray(value) || value.length > 0);
449
454
  case 'email':
@@ -465,6 +470,30 @@ class FormEnhancements {
465
470
  return parseFloat(value) <= parseFloat(rule.value);
466
471
  case 'pattern':
467
472
  return new RegExp(rule.value).test(value);
473
+ case 'file_type': {
474
+ // rule.value is a comma-separated list of allowed extensions (e.g. "pdf,doc,docx")
475
+ if (!value || !(value instanceof FileList) || value.length === 0) return true;
476
+ const allowed = rule.value.split(',').map(ext => ext.trim().toLowerCase().replace(/^\./, ''));
477
+ for (let i = 0; i < value.length; i++) {
478
+ const fileName = value[i].name || '';
479
+ const ext = fileName.split('.').pop().toLowerCase();
480
+ if (!allowed.includes(ext)) {
481
+ return false;
482
+ }
483
+ }
484
+ return true;
485
+ }
486
+ case 'file_size': {
487
+ // rule.value is maximum size in MB
488
+ if (!value || !(value instanceof FileList) || value.length === 0) return true;
489
+ const maxBytes = rule.value * 1024 * 1024;
490
+ for (let i = 0; i < value.length; i++) {
491
+ if (value[i].size > maxBytes) {
492
+ return false;
493
+ }
494
+ }
495
+ return true;
496
+ }
468
497
  case 'custom':
469
498
  return rule.validator(value);
470
499
  default:
@@ -210,34 +210,108 @@
210
210
  </div>
211
211
  </div>
212
212
 
213
+ {% if resolved_attachments %}
214
+ <div class="card mb-4">
215
+ <div class="card-header">
216
+ <h5 class="mb-0"><i class="bi bi-paperclip"></i> Attachments</h5>
217
+ </div>
218
+ <div class="card-body">
219
+ <ul class="list-unstyled mb-0">
220
+ {% for att in resolved_attachments %}
221
+ <li class="mb-2">
222
+ {% if att.url %}
223
+ <a href="{{ att.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
224
+ <i class="bi bi-download"></i> {{ att.filename }}
225
+ </a>
226
+ {% else %}
227
+ <span class="text-muted"><i class="bi bi-file-earmark"></i> {{ att.filename }}</span>
228
+ {% endif %}
229
+ </li>
230
+ {% endfor %}
231
+ </ul>
232
+ </div>
233
+ </div>
234
+ {% endif %}
235
+
213
236
  <!-- Completed Approval Steps (show previous approvals) -->
214
- {% for step in approval_steps %}
215
- {% if step.is_completed %}
216
- <div class="card mb-4 border-success">
217
- <div class="card-header bg-success text-white">
218
- <h5 class="mb-0"><i class="bi bi-check-circle"></i> Step {{ step.number }}: {{ step.group_name }}
219
- <span class="badge bg-light text-success float-end">Completed</span>
220
- </h5>
237
+ {% if approval_step_sections %}
238
+ {% for section in approval_step_sections %}
239
+ <div class="card mb-4 {% if section.status == 'approved' %}border-success{% elif section.status == 'rejected' %}border-danger{% endif %}">
240
+ <div class="card-header {% if section.status == 'approved' %}bg-success{% elif section.status == 'rejected' %}bg-danger{% else %}bg-secondary{% endif %} text-white">
241
+ <h5 class="mb-0">
242
+ <i class="bi {% if section.status == 'approved' %}bi-check-circle{% elif section.status == 'rejected' %}bi-x-circle{% else %}bi-hourglass{% endif %}"></i>
243
+ {{ section.step_name }}
244
+ {% if section.status == 'approved' %}
245
+ <span class="badge bg-light text-success float-end">Approved</span>
246
+ {% elif section.status == 'rejected' %}
247
+ <span class="badge bg-light text-danger float-end">Rejected</span>
248
+ {% endif %}
249
+ </h5>
250
+ </div>
251
+ <div class="card-body">
252
+ {% if section.completed_by %}
253
+ <div class="text-muted small mb-2">
254
+ by <strong>{{ section.completed_by }}</strong>{% if section.completed_at %} &middot; {{ section.completed_at|date:"N j, Y \a\t g:i a" }}{% endif %}
221
255
  </div>
222
- <div class="card-body">
223
- <table class="table table-bordered table-sm mb-0">
224
- {% for field_info in step.fields %}
225
- {% with field_name=field_info.field_name %}
226
- {% if field_name in form_data %}
227
- <tr>
228
- <th style="width: 30%;">{{ field_info.field_label }}</th>
229
- <td>{{ form_data|get_item:field_name }}</td>
230
- </tr>
256
+ {% endif %}
257
+ {% if section.comments %}
258
+ <p class="mb-2 small fst-italic text-secondary">&#8220;{{ section.comments }}&#8221;</p>
259
+ {% endif %}
260
+ {% if section.fields %}
261
+ <table class="table table-bordered table-sm mb-0">
262
+ {% for field in section.fields %}
263
+ <tr>
264
+ <th style="width: 30%;">{{ field.label }}</th>
265
+ <td>
266
+ {% if field.value.url %}
267
+ {% if field.value.content_type and 'image' in field.value.content_type %}
268
+ <div class="mb-2">
269
+ <img src="{{ field.value.url }}" alt="{{ field.value.filename }}" class="img-fluid" style="max-width: 400px; max-height: 400px;">
270
+ </div>
231
271
  {% endif %}
232
- {% endwith %}
233
- {% empty %}
234
- <tr><td colspan="2" class="text-muted">No fields recorded for this step.</td></tr>
235
- {% endfor %}
236
- </table>
237
- </div>
272
+ <a href="{{ field.value.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
273
+ <i class="bi bi-download"></i> {{ field.value.filename }}
274
+ </a>
275
+ <small class="text-muted ms-2">
276
+ {% if field.value.size %}({{ field.value.size|filesizeformat }}){% endif %}
277
+ </small>
278
+ {% elif field.value.path %}
279
+ <span class="text-muted">{{ field.value.filename }}</span>
280
+ <small class="text-muted ms-2">(File stored but URL unavailable)</small>
281
+ {% elif field.value.0.filename %}
282
+ <ul class="list-unstyled mb-0">
283
+ {% for f in field.value %}
284
+ <li class="mb-1">
285
+ {% if f.url %}
286
+ {% if f.content_type and 'image' in f.content_type %}
287
+ <div class="mb-1">
288
+ <img src="{{ f.url }}" alt="{{ f.filename }}" class="img-fluid" style="max-width: 200px; max-height: 200px;">
289
+ </div>
290
+ {% endif %}
291
+ <a href="{{ f.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
292
+ <i class="bi bi-download"></i> {{ f.filename }}
293
+ </a>
294
+ <small class="text-muted ms-2">{% if f.size %}({{ f.size|filesizeformat }}){% endif %}</small>
295
+ {% else %}
296
+ <span class="text-muted">{{ f.filename }}</span>
297
+ {% endif %}
298
+ </li>
299
+ {% endfor %}
300
+ </ul>
301
+ {% else %}
302
+ {{ field.value }}
303
+ {% endif %}
304
+ </td>
305
+ </tr>
306
+ {% endfor %}
307
+ </table>
308
+ {% else %}
309
+ <p class="text-muted mb-0">No fields recorded for this step.</p>
310
+ {% endif %}
238
311
  </div>
239
- {% endif %}
312
+ </div>
240
313
  {% endfor %}
314
+ {% endif %}
241
315
 
242
316
  <!-- Approval Form with Step Fields -->
243
317
  <form method="post" enctype="multipart/form-data" data-loading-message="Processing your decision...">
@@ -325,22 +399,6 @@
325
399
  </div>
326
400
  {% endif %}
327
401
 
328
- <!-- Future Steps Preview (greyed out) -->
329
- {% for step in approval_steps %}
330
- {% if step.number > current_step_number %}
331
- <div class="card mb-4 border-secondary opacity-50">
332
- <div class="card-header bg-light text-muted">
333
- <h5 class="mb-0"><i class="bi bi-hourglass"></i> Step {{ step.number }}: {{ step.group_name }}
334
- <span class="badge bg-secondary float-end">Pending</span>
335
- </h5>
336
- </div>
337
- <div class="card-body">
338
- <p class="text-muted mb-0"><em>This step will be available after all previous steps are approved.</em></p>
339
- </div>
340
- </div>
341
- {% endif %}
342
- {% endfor %}
343
-
344
402
  {% else %}
345
403
  <!-- Standard Approval (No Step Fields) -->
346
404
  <!-- Submission Data -->
@@ -380,6 +438,115 @@
380
438
  </table>
381
439
  </div>
382
440
  </div>
441
+ {% if resolved_attachments %}
442
+ <div class="card mb-4">
443
+ <div class="card-header">
444
+ <h5 class="mb-0"><i class="bi bi-paperclip"></i> Attachments</h5>
445
+ </div>
446
+ <div class="card-body">
447
+ <ul class="list-unstyled mb-0">
448
+ {% for att in resolved_attachments %}
449
+ <li class="mb-2">
450
+ {% if att.url %}
451
+ <a href="{{ att.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
452
+ <i class="bi bi-download"></i> {{ att.filename }}
453
+ </a>
454
+ {% else %}
455
+ <span class="text-muted"><i class="bi bi-file-earmark"></i> {{ att.filename }}</span>
456
+ {% endif %}
457
+ </li>
458
+ {% endfor %}
459
+ </ul>
460
+ </div>
461
+ </div>
462
+ {% endif %}
463
+
464
+ <!-- Prior Approval Step Responses (standard approval path) -->
465
+ {% if approval_step_sections %}
466
+ <div class="card mb-4">
467
+ <div class="card-header bg-light">
468
+ <h5 class="mb-0"><i class="bi bi-pencil-square"></i> Prior Approval Step Responses</h5>
469
+ </div>
470
+ <div class="card-body p-0">
471
+ {% for section in approval_step_sections %}
472
+ <div {% if not forloop.last %}class="border-bottom"{% endif %}>
473
+ <div class="px-3 pt-3 pb-1 d-flex justify-content-between align-items-start flex-wrap gap-2">
474
+ <div>
475
+ <div class="d-flex align-items-center gap-2 mb-1">
476
+ <h6 class="mb-0 fw-bold">{{ section.step_name }}</h6>
477
+ {% if section.status == "approved" %}
478
+ <span class="badge bg-success">&#10003; Approved</span>
479
+ {% elif section.status == "rejected" %}
480
+ <span class="badge bg-danger">&#10007; Rejected</span>
481
+ {% endif %}
482
+ </div>
483
+ {% if section.completed_by %}
484
+ <div class="text-muted small">
485
+ by <strong>{{ section.completed_by }}</strong>{% if section.completed_at %} &middot; {{ section.completed_at|date:"N j, Y \a\t g:i a" }}{% endif %}
486
+ </div>
487
+ {% endif %}
488
+ {% if section.comments %}
489
+ <p class="mb-0 mt-1 small fst-italic text-secondary">&#8220;{{ section.comments }}&#8221;</p>
490
+ {% endif %}
491
+ </div>
492
+ </div>
493
+ {% if section.fields %}
494
+ <div class="px-3 pb-3">
495
+ <table class="table table-bordered table-sm mb-0">
496
+ {% for field in section.fields %}
497
+ <tr>
498
+ <th style="width: 30%;">{{ field.label }}</th>
499
+ <td>
500
+ {% if field.value.url %}
501
+ {% if field.value.content_type and 'image' in field.value.content_type %}
502
+ <div class="mb-2">
503
+ <img src="{{ field.value.url }}" alt="{{ field.value.filename }}" class="img-fluid" style="max-width: 400px; max-height: 400px;">
504
+ </div>
505
+ {% endif %}
506
+ <a href="{{ field.value.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
507
+ <i class="bi bi-download"></i> {{ field.value.filename }}
508
+ </a>
509
+ <small class="text-muted ms-2">
510
+ {% if field.value.size %}({{ field.value.size|filesizeformat }}){% endif %}
511
+ </small>
512
+ {% elif field.value.path %}
513
+ <span class="text-muted">{{ field.value.filename }}</span>
514
+ <small class="text-muted ms-2">(File stored but URL unavailable)</small>
515
+ {% elif field.value.0.filename %}
516
+ <ul class="list-unstyled mb-0">
517
+ {% for f in field.value %}
518
+ <li class="mb-1">
519
+ {% if f.url %}
520
+ {% if f.content_type and 'image' in f.content_type %}
521
+ <div class="mb-1">
522
+ <img src="{{ f.url }}" alt="{{ f.filename }}" class="img-fluid" style="max-width: 200px; max-height: 200px;">
523
+ </div>
524
+ {% endif %}
525
+ <a href="{{ f.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
526
+ <i class="bi bi-download"></i> {{ f.filename }}
527
+ </a>
528
+ <small class="text-muted ms-2">{% if f.size %}({{ f.size|filesizeformat }}){% endif %}</small>
529
+ {% else %}
530
+ <span class="text-muted">{{ f.filename }}</span>
531
+ {% endif %}
532
+ </li>
533
+ {% endfor %}
534
+ </ul>
535
+ {% else %}
536
+ {{ field.value }}
537
+ {% endif %}
538
+ </td>
539
+ </tr>
540
+ {% endfor %}
541
+ </table>
542
+ </div>
543
+ {% endif %}
544
+ </div>
545
+ {% endfor %}
546
+ </div>
547
+ </div>
548
+ {% endif %}
549
+
383
550
  <!-- Approval Decision -->
384
551
  <div class="card mb-4 border-success">
385
552
  <div class="card-header bg-light">
@@ -136,6 +136,31 @@
136
136
  </div>
137
137
  </div>
138
138
 
139
+ <!-- Model-level Attachments (e.g. migrated SharePoint documents) -->
140
+ {% if resolved_attachments %}
141
+ <div class="card mb-4">
142
+ <div class="card-header">
143
+ <h5 class="mb-0"><i class="bi bi-paperclip"></i> Attachments</h5>
144
+ </div>
145
+ <div class="card-body">
146
+ <ul class="list-unstyled mb-0">
147
+ {% for att in resolved_attachments %}
148
+ <li class="mb-2">
149
+ {% if att.url %}
150
+ <a href="{{ att.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
151
+ <i class="bi bi-download"></i> {{ att.filename }}
152
+ </a>
153
+ {% else %}
154
+ <span class="text-muted"><i class="bi bi-file-earmark"></i> {{ att.filename }}</span>
155
+ <small class="text-muted ms-2">(URL unavailable)</small>
156
+ {% endif %}
157
+ </li>
158
+ {% endfor %}
159
+ </ul>
160
+ </div>
161
+ </div>
162
+ {% endif %}
163
+
139
164
  <!-- Approval Step Responses (fields filled in by each approver, separate from original submission) -->
140
165
  {% if approval_step_sections and not hide_approval_history %}
141
166
  <div class="card mb-4">
@@ -171,7 +196,49 @@
171
196
  {% for field in section.fields %}
172
197
  <tr>
173
198
  <th style="width: 30%;">{{ field.label }}</th>
174
- <td>{{ field.value }}</td>
199
+ <td>
200
+ {% if field.value.url %}
201
+ {# Single file upload with URL #}
202
+ {% if field.value.content_type and 'image' in field.value.content_type %}
203
+ <div class="mb-2">
204
+ <img src="{{ field.value.url }}" alt="{{ field.value.filename }}" class="img-fluid" style="max-width: 400px; max-height: 400px;">
205
+ </div>
206
+ {% endif %}
207
+ <a href="{{ field.value.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
208
+ <i class="bi bi-download"></i> {{ field.value.filename }}
209
+ </a>
210
+ <small class="text-muted ms-2">
211
+ {% if field.value.size %}({{ field.value.size|filesizeformat }}){% endif %}
212
+ </small>
213
+ {% elif field.value.path %}
214
+ {# Single file with path but no URL #}
215
+ <span class="text-muted">{{ field.value.filename }}</span>
216
+ <small class="text-muted ms-2">(File stored but URL unavailable)</small>
217
+ {% elif field.value.0.filename %}
218
+ {# Multi-file upload: list of file dicts #}
219
+ <ul class="list-unstyled mb-0">
220
+ {% for f in field.value %}
221
+ <li class="mb-1">
222
+ {% if f.url %}
223
+ {% if f.content_type and 'image' in f.content_type %}
224
+ <div class="mb-1">
225
+ <img src="{{ f.url }}" alt="{{ f.filename }}" class="img-fluid" style="max-width: 200px; max-height: 200px;">
226
+ </div>
227
+ {% endif %}
228
+ <a href="{{ f.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
229
+ <i class="bi bi-download"></i> {{ f.filename }}
230
+ </a>
231
+ <small class="text-muted ms-2">{% if f.size %}({{ f.size|filesizeformat }}){% endif %}</small>
232
+ {% else %}
233
+ <span class="text-muted">{{ f.filename }}</span>
234
+ {% endif %}
235
+ </li>
236
+ {% endfor %}
237
+ </ul>
238
+ {% else %}
239
+ {{ field.value }}
240
+ {% endif %}
241
+ </td>
175
242
  </tr>
176
243
  {% endfor %}
177
244
  </table>
@@ -250,6 +250,15 @@
250
250
  </tbody>
251
251
  </table>
252
252
 
253
+ {% if resolved_attachments %}
254
+ <h3 style="margin-top: 20px;">Attachments</h3>
255
+ <ul>
256
+ {% for att in resolved_attachments %}
257
+ <li>{{ att.filename }}</li>
258
+ {% endfor %}
259
+ </ul>
260
+ {% endif %}
261
+
253
262
  {% for section in approval_step_sections %}
254
263
  <div class="step-section-header">
255
264
  <span class="step-name-cell">{{ section.step_name }}</span>