django-forms-workflows 0.77.4__tar.gz → 0.78.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 (235) hide show
  1. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/CHANGELOG.md +24 -0
  2. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/PKG-INFO +12 -7
  3. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/README.md +5 -3
  4. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/admin.py +23 -0
  5. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/api_views.py +1 -1
  6. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/apps.py +1 -1
  7. django_forms_workflows-0.78.0/django_forms_workflows/checks.py +24 -0
  8. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/diff_views.py +2 -0
  9. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/form_builder_views.py +14 -0
  10. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/forms.py +6 -25
  11. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/models.py +3 -0
  12. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/payments/__init__.py +8 -1
  13. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/payments/base.py +24 -0
  14. django_forms_workflows-0.78.0/django_forms_workflows/payments/checkout.py +84 -0
  15. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/payments/stripe_provider.py +38 -13
  16. django_forms_workflows-0.78.0/django_forms_workflows/payments/views.py +484 -0
  17. django_forms_workflows-0.78.0/django_forms_workflows/pdf.py +53 -0
  18. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/reporting_views.py +44 -19
  19. django_forms_workflows-0.78.0/django_forms_workflows/security.py +108 -0
  20. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/sso_backends.py +30 -67
  21. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/sso_views.py +8 -75
  22. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/admin/django_forms_workflows/workflow_builder.html +2 -2
  23. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/payment_collect.html +8 -2
  24. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/payment_error.html +6 -3
  25. django_forms_workflows-0.78.0/django_forms_workflows/uploads.py +104 -0
  26. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/utils.py +43 -31
  27. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/views.py +66 -108
  28. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/workflow_builder_views.py +17 -6
  29. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/pyproject.toml +11 -5
  30. django_forms_workflows-0.77.4/django_forms_workflows/payments/views.py +0 -358
  31. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/LICENSE +0 -0
  32. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/__init__.py +0 -0
  33. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/api_urls.py +0 -0
  34. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/callback_registry.py +0 -0
  35. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/conditions.py +0 -0
  36. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/context_processors.py +0 -0
  37. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/data_sources/__init__.py +0 -0
  38. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/data_sources/base.py +0 -0
  39. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/data_sources/database_source.py +0 -0
  40. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/data_sources/ldap_source.py +0 -0
  41. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/data_sources/user_source.py +0 -0
  42. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/email_backends/__init__.py +0 -0
  43. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/email_backends/gmail_api.py +0 -0
  44. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/form_builder_urls.py +0 -0
  45. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/handlers/__init__.py +0 -0
  46. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/handlers/api_handler.py +0 -0
  47. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/handlers/base.py +0 -0
  48. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/handlers/database_handler.py +0 -0
  49. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/handlers/email_handler.py +0 -0
  50. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/handlers/executor.py +0 -0
  51. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/handlers/file_handler.py +0 -0
  52. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/handlers/ldap_handler.py +0 -0
  53. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/history.py +0 -0
  54. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/ldap_backend.py +0 -0
  55. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/management/commands/create_default_templates.py +0 -0
  56. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/management/commands/pull_forms.py +0 -0
  57. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/management/commands/push_forms.py +0 -0
  58. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/management/commands/seed_farm_demo.py +0 -0
  59. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/management/commands/seed_pcn_form.py +0 -0
  60. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/management/commands/seed_prefill_sources.py +0 -0
  61. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/management/commands/sync_ldap_profiles.py +0 -0
  62. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/management/commands/sync_ldap_users.py +0 -0
  63. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/management/commands/test_db_connection.py +0 -0
  64. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0001_initial.py +0 -0
  65. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0002_prefillsource_alter_formfield_prefill_source_and_more.py +0 -0
  66. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0003_postsubmissionaction.py +0 -0
  67. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0004_formtemplate.py +0 -0
  68. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0005_advanced_conditional_logic.py +0 -0
  69. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0006_workflowdefinition_visual_workflow_data_and_more.py +0 -0
  70. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0007_add_userprofile_ldap_enhancements.py +0 -0
  71. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0008_add_file_workflow_models.py +0 -0
  72. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0009_add_formfield_readonly.py +0 -0
  73. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0010_add_prefillsource_template_fields.py +0 -0
  74. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0011_postsubmissionaction_email_body_template_and_more.py +0 -0
  75. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0012_add_approval_step_fields.py +0 -0
  76. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0013_add_database_query_key.py +0 -0
  77. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0014_add_form_category.py +0 -0
  78. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0015_add_workflowstage_staged_approvals.py +0 -0
  79. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0016_add_pdf_generation.py +0 -0
  80. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0017_add_notification_batching.py +0 -0
  81. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0018_add_form_category_parent.py +0 -0
  82. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0019_add_ldap_group_profile.py +0 -0
  83. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0020_add_bulk_export_field.py +0 -0
  84. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0021_alter_pendingnotification_scheduled_for_and_more.py +0 -0
  85. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0022_add_bulk_pdf_export.py +0 -0
  86. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0023_add_performance_indexes.py +0 -0
  87. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0024_add_allow_resubmit.py +0 -0
  88. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0025_workflowstage_approve_label.py +0 -0
  89. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0026_alter_formfield_width.py +0 -0
  90. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0027_formsubmission_form_data_gin.py +0 -0
  91. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0028_workflowstagegroupconfig.py +0 -0
  92. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0029_formfield_workflow_stage_fk_drop_groupconfig.py +0 -0
  93. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0030_formfield_phone_type.py +0 -0
  94. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0031_sub_workflows.py +0 -0
  95. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0032_approved_pending_detached_sub_workflow.py +0 -0
  96. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0033_add_reject_parent_to_subworkflowdefinition.py +0 -0
  97. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0034_add_notificationlog.py +0 -0
  98. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0035_migrate_prefill_source_strings.py +0 -0
  99. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0036_add_workflowstage_auto_created.py +0 -0
  100. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0037_migrate_flat_workflows_to_staged.py +0 -0
  101. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0038_remove_legacy_prefill_source_and_approval_step.py +0 -0
  102. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0039_update_help_text.py +0 -0
  103. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0040_migrate_deprecated_features.py +0 -0
  104. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0041_remove_deprecated_fields.py +0 -0
  105. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0042_add_sub_workflow_section_label.py +0 -0
  106. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0043_fix_form_data_decimal_encoding.py +0 -0
  107. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0044_remove_subworkflowinstance_label.py +0 -0
  108. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0045_stageapprovalgroup_workflow_name_label.py +0 -0
  109. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0046_workflow_definition_one_to_many.py +0 -0
  110. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0047_add_hide_approval_history.py +0 -0
  111. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0048_workflowdefinition_trigger_conditions_and_more.py +0 -0
  112. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0049_formfield_multifile_type.py +0 -0
  113. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0050_workflowdefinition_collapse_parallel_stages.py +0 -0
  114. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0051_formdefinition_is_listed.py +0 -0
  115. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0052_dynamic_assignee_and_form_field_notifications.py +0 -0
  116. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0053_stageformfieldnotification_static_emails.py +0 -0
  117. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0054_rename_approved_pending_to_pending_approval.py +0 -0
  118. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0055_add_send_back.py +0 -0
  119. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0056_calculated_formula_spreadsheet.py +0 -0
  120. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0057_add_currency_field_type.py +0 -0
  121. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0058_rename_assignee_email_field_add_lookup_type.py +0 -0
  122. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0059_add_multiselect_list_field_type.py +0 -0
  123. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0060_alter_workflowstage_assignee_lookup_type_helptext.py +0 -0
  124. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0061_add_country_us_state_field_types.py +0 -0
  125. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0062_add_batch_import_to_formdefinition.py +0 -0
  126. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0063_formdefinition_api_enabled_apitoken.py +0 -0
  127. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0064_add_reviewer_groups.py +0 -0
  128. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0065_workflownotification.py +0 -0
  129. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0066_add_notify_submitter_to_workflownotification.py +0 -0
  130. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0067_migrate_legacy_notify_flags.py +0 -0
  131. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0068_drop_legacy_notify_fields.py +0 -0
  132. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0069_add_signature_field_type.py +0 -0
  133. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0070_workflowstage_allow_edit_form_data.py +0 -0
  134. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0071_alter_pendingnotification_notification_type.py +0 -0
  135. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0072_add_change_history.py +0 -0
  136. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0073_add_stage_notify_assignee_on_final_decision.py +0 -0
  137. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0074_add_notification_rule.py +0 -0
  138. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0075_migrate_to_notification_rules.py +0 -0
  139. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0076_update_notification_event_choices.py +0 -0
  140. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0077_drop_legacy_notification_models.py +0 -0
  141. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0078_allow_anonymous_submissions.py +0 -0
  142. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0079_webhookendpoint_webhookdeliverylog.py +0 -0
  143. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0080_add_rating_matrix_address_slider_fields_and_form_controls.py +0 -0
  144. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0081_add_success_page_fields.py +0 -0
  145. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0082_add_document_template_model.py +0 -0
  146. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0083_workflowdefinition_start_trigger.py +0 -0
  147. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0084_add_shared_option_list.py +0 -0
  148. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0085_add_payment_system.py +0 -0
  149. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0086_add_embed_enabled.py +0 -0
  150. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0087_notificationrule_use_triggering_stage.py +0 -0
  151. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0088_notificationrule_body_template_and_events.py +0 -0
  152. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0089_workflowstage_hide_comment_field.py +0 -0
  153. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0090_alter_stageapprovalgroup_options_and_more.py +0 -0
  154. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0091_add_uuid_fields.py +0 -0
  155. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0092_add_user_notification_preference.py +0 -0
  156. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0093_normalize_conditional_operators.py +0 -0
  157. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0094_formfield_show_help_text_in_detail.py +0 -0
  158. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0095_notificationrule_cc_fields.py +0 -0
  159. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0096_notificationlog_cc_bcc_emails.py +0 -0
  160. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/0097_notificationlog_delivery_reconciliation.py +0 -0
  161. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/migrations/__init__.py +0 -0
  162. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/payments/registry.py +0 -0
  163. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/signals.py +0 -0
  164. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/sso_urls.py +0 -0
  165. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/api/swagger-ui-bundle.js +0 -0
  166. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/api/swagger-ui.css +0 -0
  167. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/css/forms.css +0 -0
  168. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/admin_notification_rule_stage_filter.js +0 -0
  169. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/dfw-embed.js +0 -0
  170. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/form-builder-api.js +0 -0
  171. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/form-builder-canvas.js +0 -0
  172. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/form-builder-history.js +0 -0
  173. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/form-builder-property-editor.js +0 -0
  174. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/form-builder-store.js +0 -0
  175. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/form-builder.js +0 -0
  176. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/form-enhancements.js +0 -0
  177. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/payment-stripe.js +0 -0
  178. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/signature-pad.js +0 -0
  179. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/workflow-builder-api.js +0 -0
  180. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/workflow-builder-store.js +0 -0
  181. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/static/django_forms_workflows/js/workflow-builder.js +0 -0
  182. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/sync_api.py +0 -0
  183. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/sync_views.py +0 -0
  184. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/tasks.py +0 -0
  185. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/admin/django_forms_workflows/_sync_diff.html +0 -0
  186. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/admin/django_forms_workflows/diff_forms.html +0 -0
  187. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/admin/django_forms_workflows/form_builder.html +0 -0
  188. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/admin/django_forms_workflows/formdef_change_form.html +0 -0
  189. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/admin/django_forms_workflows/formdefinition/change_list.html +0 -0
  190. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_import.html +0 -0
  191. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_pull.html +0 -0
  192. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_push.html +0 -0
  193. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/_category_node.html +0 -0
  194. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/_field_value.html +0 -0
  195. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/_form_data_rows.html +0 -0
  196. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/analytics_dashboard.html +0 -0
  197. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/api/docs.html +0 -0
  198. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/approval_inbox.html +0 -0
  199. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/approve.html +0 -0
  200. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/base.html +0 -0
  201. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/batch_import_result.html +0 -0
  202. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/completed_approvals.html +0 -0
  203. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/discard_draft_confirm.html +0 -0
  204. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/embed_base.html +0 -0
  205. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/embed_success.html +0 -0
  206. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/form_categories.html +0 -0
  207. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/form_embed.html +0 -0
  208. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/form_list.html +0 -0
  209. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/form_submit.html +0 -0
  210. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/my_submissions.html +0 -0
  211. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/notification_preferences.html +0 -0
  212. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/public_submission_confirmation.html +0 -0
  213. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/rate_limited.html +0 -0
  214. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/reassign_task.html +0 -0
  215. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/resubmit_confirm.html +0 -0
  216. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/sso/login.html +0 -0
  217. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/sub_workflow_detail.html +0 -0
  218. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/submission_bulk_pdf.html +0 -0
  219. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/submission_detail.html +0 -0
  220. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/submission_pdf.html +0 -0
  221. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/submission_success.html +0 -0
  222. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/django_forms_workflows/withdraw_confirm.html +0 -0
  223. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/emails/approval_notification.html +0 -0
  224. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/emails/approval_reminder.html +0 -0
  225. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/emails/approval_request.html +0 -0
  226. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/emails/email_styles.html +0 -0
  227. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/emails/escalation_notification.html +0 -0
  228. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/emails/notification_digest.html +0 -0
  229. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/emails/rejection_notification.html +0 -0
  230. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/emails/submission_notification.html +0 -0
  231. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templates/registration/login.html +0 -0
  232. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templatetags/__init__.py +0 -0
  233. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/templatetags/forms_workflows_tags.py +0 -0
  234. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/urls.py +0 -0
  235. {django_forms_workflows-0.77.4 → django_forms_workflows-0.78.0}/django_forms_workflows/workflow_engine.py +0 -0
@@ -8,6 +8,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
  ### Security
11
+ - **Payment endpoints hardened against unauthorized access and unsafe state
12
+ transitions (issue #27, supersedes #4).** All checkout endpoints
13
+ (`initiate`, `confirm`, `return`, `cancel`) now enforce one access policy
14
+ before touching records or providers: an authenticated submitter may act
15
+ only on their own submission, and an anonymous public-form submitter needs
16
+ a short-lived (`FORMS_WORKFLOWS_PAYMENT_CHECKOUT_TTL`, default 1 hour),
17
+ unguessable session-bound checkout credential issued when the form post
18
+ enters `pending_payment`. Guessing a numeric submission or payment-record
19
+ ID now yields a non-disclosing 404. Payment initiation is idempotent per
20
+ attempt — repeated GETs reuse the single open `PaymentRecord` and its
21
+ stable idempotency key (now forwarded to Stripe's `PaymentIntent.create`)
22
+ instead of creating uncontrolled attempts; a legitimate retry after
23
+ cancel/failure starts a fresh key. Cancellation is a CSRF-protected
24
+ `POST`-only action restricted to `pending_payment` submissions and can no
25
+ longer revert a completed/submitted/approved submission to `draft`.
26
+ Stripe webhooks now **fail closed**: events are rejected (HTTP 400) when
27
+ `STRIPE_WEBHOOK_SECRET` is unset or the signature is invalid, and the
28
+ provider interface requires the same authenticated-event guarantee from
29
+ all providers via `WebhookVerificationError`. Replayed or out-of-order
30
+ events no longer roll payment state backward (`completed`/`refunded` are
31
+ terminal; only `completed → refunded` is allowed) and duplicate deliveries
32
+ create no duplicate workflow tasks. `payment_collect.html` and
33
+ `payment_error.html` cancel actions are now POST forms.
34
+
11
35
  - **`social-auth-core` 4.8.3 → 5.1.1 in `poetry.lock`.** Closes five Dependabot
12
36
  advisories against the locked version: GHSA-vqg6-3fw6-j9jg (session fixation),
13
37
  GHSA-3c93-f73f-qc9h (VK App backend accepted unsigned callback data),
@@ -1,22 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-forms-workflows
3
- Version: 0.77.4
3
+ Version: 0.78.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
7
7
  Keywords: django,forms,workflows,approval,form-builder,dynamic-forms,ldap,enterprise,audit,database-driven,sso,saml,oauth2
8
8
  Author: Matt Davis
9
9
  Author-email: matt@opensensor.io
10
- Requires-Python: >=3.10,<4.0
10
+ Requires-Python: >=3.11,<4.0
11
11
  Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
17
16
  Classifier: Programming Language :: Python :: 3.14
18
17
  Classifier: Programming Language :: Python :: 3.15
19
18
  Provides-Extra: all
19
+ Provides-Extra: cache
20
20
  Provides-Extra: dev
21
21
  Provides-Extra: docs
22
22
  Provides-Extra: excel
@@ -31,7 +31,7 @@ Provides-Extra: postgresql
31
31
  Provides-Extra: qr
32
32
  Provides-Extra: saml
33
33
  Provides-Extra: sso
34
- Requires-Dist: Django (>=5.1,<7.0)
34
+ Requires-Dist: Django (>=5.2.18,<7.0,!=6.0.0,!=6.0.1,!=6.0.2,!=6.0.3,!=6.0.4,!=6.0.5,!=6.0.6,!=6.0.7,!=6.0.8,!=6.1.0,!=6.1.1)
35
35
  Requires-Dist: celery (>=5.3)
36
36
  Requires-Dist: crispy-bootstrap5 (>=2.0)
37
37
  Requires-Dist: django-auth-ldap (>=4.0) ; extra == "ldap" or extra == "all"
@@ -44,7 +44,9 @@ Requires-Dist: google-auth (>=2.20) ; extra == "gmail" or extra == "all"
44
44
  Requires-Dist: markdown (>=3.4) ; extra == "markdown" or extra == "all"
45
45
  Requires-Dist: mssql-django (>=1.3) ; extra == "mssql" or extra == "all"
46
46
  Requires-Dist: mysqlclient (>=2.2) ; extra == "mysql" or extra == "all"
47
+ Requires-Dist: nh3 (>=0.3.2)
47
48
  Requires-Dist: openpyxl (>=3.1) ; extra == "excel" or extra == "all"
49
+ Requires-Dist: pillow (>=13.0.0) ; extra == "pdf" or extra == "all"
48
50
  Requires-Dist: psycopg2-binary (>=2.9) ; extra == "postgresql" or extra == "all"
49
51
  Requires-Dist: pyodbc (>=5.0) ; extra == "mssql" or extra == "all"
50
52
  Requires-Dist: pytest (>=7.4) ; extra == "dev"
@@ -53,6 +55,7 @@ Requires-Dist: pytest-django (>=4.5) ; extra == "dev"
53
55
  Requires-Dist: python-decouple (>=3.8)
54
56
  Requires-Dist: python-ldap (>=3.4) ; extra == "ldap" or extra == "all"
55
57
  Requires-Dist: python3-saml (>=1.16) ; extra == "sso" or extra == "saml" or extra == "all"
58
+ Requires-Dist: redis (>=5.0) ; extra == "cache" or extra == "all"
56
59
  Requires-Dist: requests (>=2.31)
57
60
  Requires-Dist: ruff (>=0.4) ; extra == "dev"
58
61
  Requires-Dist: segno (>=1.6) ; extra == "qr" or extra == "all"
@@ -72,7 +75,7 @@ Description-Content-Type: text/markdown
72
75
  **Enterprise-grade, database-driven form builder with multi-stage approval workflows, external data integration, and cross-instance sync**
73
76
 
74
77
  [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
75
- [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
78
+ [![Python Version](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
76
79
  [![Django Version](https://img.shields.io/badge/django-5.2%2B-green)](https://www.djangoproject.com/)
77
80
  [![Version](https://img.shields.io/badge/version-0.49.0-orange)](https://github.com/opensensor/django-forms-workflows)
78
81
 
@@ -258,6 +261,8 @@ Move form definitions between environments from the Django Admin:
258
261
 
259
262
  ## Quick Start
260
263
 
264
+ See [security upgrade notes](docs/SECURITY_UPGRADE.md) before upgrading an existing deployment.
265
+
261
266
  ### Installation
262
267
 
263
268
  ```bash
@@ -413,8 +418,8 @@ graph TB
413
418
 
414
419
  ## Requirements
415
420
 
416
- - Python 3.10+
417
- - Django 5.2+
421
+ - Python 3.11+
422
+ - Django 5.2.18+, 6.0.9+, or 6.1.2+ (security patch minimums)
418
423
  - PostgreSQL, MySQL, or SQLite (PostgreSQL recommended for production)
419
424
  - Optional: Celery 5.0+ with Redis/Valkey for background task processing
420
425
  - Optional: `openpyxl` for Excel spreadsheet field support (`pip install django-forms-workflows[excel]`)
@@ -3,7 +3,7 @@
3
3
  **Enterprise-grade, database-driven form builder with multi-stage approval workflows, external data integration, and cross-instance sync**
4
4
 
5
5
  [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
6
- [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
6
+ [![Python Version](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
7
7
  [![Django Version](https://img.shields.io/badge/django-5.2%2B-green)](https://www.djangoproject.com/)
8
8
  [![Version](https://img.shields.io/badge/version-0.49.0-orange)](https://github.com/opensensor/django-forms-workflows)
9
9
 
@@ -189,6 +189,8 @@ Move form definitions between environments from the Django Admin:
189
189
 
190
190
  ## Quick Start
191
191
 
192
+ See [security upgrade notes](docs/SECURITY_UPGRADE.md) before upgrading an existing deployment.
193
+
192
194
  ### Installation
193
195
 
194
196
  ```bash
@@ -344,8 +346,8 @@ graph TB
344
346
 
345
347
  ## Requirements
346
348
 
347
- - Python 3.10+
348
- - Django 5.2+
349
+ - Python 3.11+
350
+ - Django 5.2.18+, 6.0.9+, or 6.1.2+ (security patch minimums)
349
351
  - PostgreSQL, MySQL, or SQLite (PostgreSQL recommended for production)
350
352
  - Optional: Celery 5.0+ with Redis/Valkey for background task processing
351
353
  - Optional: `openpyxl` for Excel spreadsheet field support (`pip install django-forms-workflows[excel]`)
@@ -1154,6 +1154,15 @@ class FormDefinitionAdmin(nested_admin.NestedModelAdmin):
1154
1154
 
1155
1155
  def sync_import_admin_view(self, request):
1156
1156
  """Admin page for importing form definitions from a JSON file or raw JSON."""
1157
+ from django.core.exceptions import PermissionDenied
1158
+
1159
+ if not self.has_view_permission(request):
1160
+ raise PermissionDenied
1161
+ if not (
1162
+ self.has_add_permission(request) and self.has_change_permission(request)
1163
+ ):
1164
+ raise PermissionDenied
1165
+
1157
1166
  from .sync_api import import_payload
1158
1167
 
1159
1168
  context = dict(self.admin_site.each_context(request))
@@ -1219,6 +1228,15 @@ class FormDefinitionAdmin(nested_admin.NestedModelAdmin):
1219
1228
  Step 1 (POST) – fetch available forms from the remote, show checkbox list
1220
1229
  Step 2 (POST) – import selected forms, show results
1221
1230
  """
1231
+ from django.core.exceptions import PermissionDenied
1232
+
1233
+ if not self.has_view_permission(request):
1234
+ raise PermissionDenied
1235
+ if not (
1236
+ self.has_add_permission(request) and self.has_change_permission(request)
1237
+ ):
1238
+ raise PermissionDenied
1239
+
1222
1240
  from .sync_api import (
1223
1241
  build_export_payload,
1224
1242
  fetch_remote_payload,
@@ -1372,6 +1390,11 @@ class FormDefinitionAdmin(nested_admin.NestedModelAdmin):
1372
1390
  Step 1 (POST) – show all local forms with checkboxes + diff status
1373
1391
  Step 2 (POST) – execute push for selected forms, show results
1374
1392
  """
1393
+ from django.core.exceptions import PermissionDenied
1394
+
1395
+ if not self.has_view_permission(request):
1396
+ raise PermissionDenied
1397
+
1375
1398
  from .sync_api import (
1376
1399
  build_export_payload,
1377
1400
  fetch_remote_payload,
@@ -106,7 +106,7 @@ def require_api_token(view_func):
106
106
  raw = auth.removeprefix("Bearer ").strip()
107
107
  try:
108
108
  token_obj = APIToken.objects.select_related("user").get(
109
- token=raw, is_active=True
109
+ token=raw, is_active=True, user__is_active=True
110
110
  )
111
111
  except (APIToken.DoesNotExist, ValueError, Exception): # noqa: BLE001
112
112
  return JsonResponse({"error": "Invalid or inactive token."}, status=401)
@@ -17,7 +17,7 @@ class DjangoFormsWorkflowsConfig(AppConfig):
17
17
  Import signal handlers and perform app initialization.
18
18
  """
19
19
  # Import signals to register handlers
20
- from . import signals # noqa: F401
20
+ from . import checks, signals # noqa: F401
21
21
 
22
22
  # Load custom callback handlers from FORMS_WORKFLOWS_CALLBACKS setting
23
23
  from .callback_registry import load_from_settings
@@ -0,0 +1,24 @@
1
+ """Deployment checks for public submission throttling."""
2
+
3
+ from django.conf import settings
4
+ from django.core.checks import Tags, Warning, register
5
+
6
+
7
+ @register(Tags.security, deploy=True)
8
+ def check_rate_limit_cache(app_configs, **kwargs):
9
+ alias = getattr(settings, "FORMS_WORKFLOWS_RATE_LIMIT_CACHE", "default")
10
+ backend = settings.CACHES.get(alias, {}).get("BACKEND", "")
11
+ if backend not in {
12
+ "django.core.cache.backends.redis.RedisCache",
13
+ "django.core.cache.backends.memcached.PyMemcacheCache",
14
+ "django.core.cache.backends.memcached.PyLibMCCache",
15
+ "django_redis.cache.RedisCache",
16
+ }:
17
+ return [
18
+ Warning(
19
+ "Public form rate limiting requires a shared cache with atomic add/incr.",
20
+ hint="Configure Redis or Memcached and FORMS_WORKFLOWS_RATE_LIMIT_CACHE; local-memory caches only limit one process.",
21
+ id="django_forms_workflows.W001",
22
+ )
23
+ ]
24
+ return []
@@ -8,6 +8,7 @@ from django.http import HttpResponseBadRequest
8
8
  from django.shortcuts import render
9
9
 
10
10
  from .models import FormDefinition
11
+ from .security import builder_permission_required
11
12
  from .sync_api import build_export_payload
12
13
 
13
14
 
@@ -429,6 +430,7 @@ def _build_summary(forms_data):
429
430
  return summaries
430
431
 
431
432
 
433
+ @builder_permission_required("view_formdefinition")
432
434
  def diff_forms_view(request):
433
435
  """Render a side-by-side JSON diff of selected FormDefinitions."""
434
436
  pks = request.GET.get("pks", "")
@@ -26,11 +26,13 @@ from .models import (
26
26
  WorkflowDefinition,
27
27
  WorkflowStage,
28
28
  )
29
+ from .security import builder_permission_required
29
30
 
30
31
  logger = logging.getLogger(__name__)
31
32
 
32
33
 
33
34
  @require_GET
35
+ @builder_permission_required("view_formtemplate")
34
36
  def form_builder_templates(request):
35
37
  """
36
38
  API endpoint to list available form templates.
@@ -63,6 +65,7 @@ def form_builder_templates(request):
63
65
 
64
66
 
65
67
  @require_GET
68
+ @builder_permission_required("view_formtemplate")
66
69
  def form_builder_load_template(request, template_id):
67
70
  """
68
71
  API endpoint to load a form template.
@@ -83,6 +86,7 @@ def form_builder_load_template(request, template_id):
83
86
 
84
87
 
85
88
  @require_POST
89
+ @builder_permission_required("view_formdefinition", "add_formdefinition")
86
90
  def form_builder_clone(request, form_id):
87
91
  """
88
92
  API endpoint to clone an existing form.
@@ -225,6 +229,7 @@ def form_builder_clone(request, form_id):
225
229
 
226
230
 
227
231
  @require_GET
232
+ @builder_permission_required("view_formdefinition")
228
233
  def form_builder_view(request, form_id=None):
229
234
  """
230
235
  Main form builder page.
@@ -269,6 +274,7 @@ def form_builder_view(request, form_id=None):
269
274
 
270
275
 
271
276
  @require_GET
277
+ @builder_permission_required("view_formdefinition")
272
278
  def form_builder_load(request, form_id):
273
279
  """
274
280
  API endpoint to load form data as JSON.
@@ -353,6 +359,7 @@ def form_builder_load(request, form_id):
353
359
 
354
360
 
355
361
  @require_POST
362
+ @builder_permission_required("save_formdefinition")
356
363
  def form_builder_save(request):
357
364
  """
358
365
  API endpoint to save form data.
@@ -604,6 +611,7 @@ def save_form_definition_from_builder_data(data, user, form_definition=None):
604
611
 
605
612
 
606
613
  @require_POST
614
+ @builder_permission_required("view_formdefinition")
607
615
  def form_builder_preview(request):
608
616
  """
609
617
  API endpoint to generate a preview of the form.
@@ -764,6 +772,7 @@ def form_builder_preview(request):
764
772
 
765
773
 
766
774
  @require_GET
775
+ @builder_permission_required("view_formdefinition", "view_documenttemplate")
767
776
  def document_template_list(request, form_id):
768
777
  """List document templates for a form."""
769
778
  templates = DocumentTemplate.objects.filter(form_definition_id=form_id).order_by(
@@ -789,6 +798,7 @@ def document_template_list(request, form_id):
789
798
 
790
799
 
791
800
  @require_POST
801
+ @builder_permission_required("change_formdefinition", "save_documenttemplate")
792
802
  def document_template_save(request, form_id):
793
803
  """Create or update a document template."""
794
804
  try:
@@ -847,6 +857,7 @@ def document_template_save(request, form_id):
847
857
 
848
858
 
849
859
  @require_POST
860
+ @builder_permission_required("change_formdefinition", "delete_documenttemplate")
850
861
  def document_template_delete(request, form_id, template_id):
851
862
  """Delete a document template."""
852
863
  tpl = get_object_or_404(
@@ -862,6 +873,7 @@ def document_template_delete(request, form_id, template_id):
862
873
 
863
874
 
864
875
  @require_GET
876
+ @builder_permission_required("view_sharedoptionlist")
865
877
  def shared_option_list_api(request):
866
878
  """List all shared option lists (for form builder dropdowns)."""
867
879
  lists = SharedOptionList.objects.filter(is_active=True).order_by("name")
@@ -882,6 +894,7 @@ def shared_option_list_api(request):
882
894
 
883
895
 
884
896
  @require_POST
897
+ @builder_permission_required("save_sharedoptionlist")
885
898
  def shared_option_list_save(request):
886
899
  """Create or update a shared option list."""
887
900
  try:
@@ -924,6 +937,7 @@ def shared_option_list_save(request):
924
937
 
925
938
 
926
939
  @require_POST
940
+ @builder_permission_required("delete_sharedoptionlist")
927
941
  def shared_option_list_delete(request, list_id):
928
942
  """Delete a shared option list."""
929
943
  ol = get_object_or_404(SharedOptionList, id=list_id)
@@ -333,18 +333,6 @@ class DynamicForm(forms.Form):
333
333
  # files). Also used to carry forward draft / resubmit file data.
334
334
  self.stashed_files = stashed_files or {}
335
335
 
336
- # Recover stashed-file metadata carried forward via hidden input from
337
- # a prior validation-failure render (browsers cannot re-send files).
338
- if not self.stashed_files and self.data:
339
- prev = self.data.get("_stashed_files")
340
- if prev:
341
- try:
342
- self.stashed_files = json.loads(prev)
343
- except (json.JSONDecodeError, TypeError):
344
- # Ignore malformed client metadata; uploaded files are rebuilt
345
- # from validated server-side data below.
346
- pass
347
-
348
336
  # Build form fields from definition
349
337
  # Exclude fields scoped to a workflow stage - those are for approvers only
350
338
  for field in (
@@ -354,15 +342,6 @@ class DynamicForm(forms.Form):
354
342
  ):
355
343
  self.add_field(field, initial_data)
356
344
 
357
- # Embed stashed-file metadata as a hidden field so it survives
358
- # validation-failure round-trips (browsers cannot re-send files).
359
- if self.stashed_files:
360
- self.fields["_stashed_files"] = forms.CharField(
361
- widget=forms.HiddenInput(),
362
- required=False,
363
- initial=json.dumps(self.stashed_files),
364
- )
365
-
366
345
  # Setup form layout with Crispy Forms
367
346
  self.helper = FormHelper()
368
347
  self.helper.form_method = "post"
@@ -763,7 +742,7 @@ class DynamicForm(forms.Form):
763
742
  # failure, saved with a draft, or carried from a resubmission).
764
743
  existing_file = self._get_existing_file_info(field_def.field_name)
765
744
  if existing_file:
766
- fname = existing_file.get("filename", "file")
745
+ fname = html_escape(existing_file.get("filename", "file"))
767
746
  extra_help = f"Previously uploaded: <strong>{fname}</strong>. Leave blank to keep it."
768
747
  base_help = field_args.get("help_text", "") or ""
769
748
  field_args["help_text"] = (
@@ -789,10 +768,11 @@ class DynamicForm(forms.Form):
789
768
  if isinstance(existing_file, list)
790
769
  else existing_file.get("filename", "file")
791
770
  )
792
- extra = f"Previously uploaded: <strong>{names}</strong>. Leave blank to keep."
771
+ extra = f"Previously uploaded: <strong>{html_escape(names)}</strong>. Leave blank to keep."
793
772
  help_text = f"{help_text} {extra}" if help_text else extra
794
773
  required = False
795
774
  self.fields[field_def.field_name] = MultipleFileField(
775
+ validators=_build_file_validators(field_def),
796
776
  required=required,
797
777
  label=field_def.field_label,
798
778
  help_text=help_text,
@@ -931,12 +911,13 @@ class DynamicForm(forms.Form):
931
911
  )
932
912
 
933
913
  elif field_def.field_type == "display_text":
934
- display_html = _render_display_text(initial or field_def.default_value)
914
+ display_html = _render_display_text(field_def.default_value)
935
915
  self.fields[field_def.field_name] = forms.CharField(
936
916
  required=False,
937
917
  label=field_def.field_label,
938
918
  help_text=field_def.help_text,
939
- initial=initial or field_def.default_value,
919
+ initial=field_def.default_value,
920
+ disabled=True,
940
921
  widget=DisplayTextWidget(
941
922
  display_html=display_html,
942
923
  attrs={"data-display-text": "true"},
@@ -881,6 +881,8 @@ class DocumentTemplate(models.Model):
881
881
  """Render this template with submission data, returning HTML string."""
882
882
  import re
883
883
 
884
+ from django.utils.html import escape
885
+
884
886
  form_data = submission.form_data or {}
885
887
 
886
888
  # Build merge context with system variables + form data
@@ -906,6 +908,7 @@ class DocumentTemplate(models.Model):
906
908
  else:
907
909
  context[key] = str(val) if val is not None else ""
908
910
 
911
+ context = {key: str(escape(value)) for key, value in context.items()}
909
912
  html = self.html_content
910
913
 
911
914
  # Process conditional blocks: {% if field_name %}...{% endif %}
@@ -12,7 +12,13 @@ apps can add ECSI, PayPal, Authorize.Net, or any other provider
12
12
  without modifying the library itself.
13
13
  """
14
14
 
15
- from .base import PaymentFlow, PaymentProvider, PaymentResult, PaymentStatus
15
+ from .base import (
16
+ PaymentFlow,
17
+ PaymentProvider,
18
+ PaymentResult,
19
+ PaymentStatus,
20
+ WebhookVerificationError,
21
+ )
16
22
  from .registry import (
17
23
  clear,
18
24
  get_available_providers,
@@ -27,6 +33,7 @@ __all__ = [
27
33
  "PaymentProvider",
28
34
  "PaymentResult",
29
35
  "PaymentStatus",
36
+ "WebhookVerificationError",
30
37
  "clear",
31
38
  "get_available_providers",
32
39
  "get_provider",
@@ -47,6 +47,17 @@ class PaymentStatus(Enum):
47
47
  REFUNDED = "refunded"
48
48
 
49
49
 
50
+ class WebhookVerificationError(Exception):
51
+ """
52
+ Raised by a provider's ``handle_webhook()`` when the event payload
53
+ cannot be authenticated (e.g. a missing/invalid signature, or a
54
+ required verification secret that is not configured).
55
+
56
+ Providers must raise this instead of processing unverified payloads:
57
+ the webhook endpoint fails closed and discards the event.
58
+ """
59
+
60
+
50
61
  @dataclass
51
62
  class PaymentResult:
52
63
  """Returned by create_payment() and confirm_payment()."""
@@ -93,12 +104,19 @@ class PaymentProvider(ABC):
93
104
  submission_id: int,
94
105
  description: str = "",
95
106
  metadata: dict[str, Any] | None = None,
107
+ idempotency_key: str | None = None,
96
108
  ) -> PaymentResult:
97
109
  """
98
110
  Initiate a payment.
99
111
 
100
112
  For INLINE providers: creates a payment intent, returns client_secret.
101
113
  For REDIRECT providers: creates a session, returns redirect_url.
114
+
115
+ ``idempotency_key`` is stable for a single payment attempt (one
116
+ ``PaymentRecord``). Providers that support request idempotency
117
+ should forward it so a retried initiation cannot create duplicate
118
+ provider-side charges. A new attempt (after a cancellation or
119
+ failure) is issued a fresh key.
102
120
  """
103
121
 
104
122
  @abstractmethod
@@ -121,6 +139,12 @@ class PaymentProvider(ABC):
121
139
 
122
140
  Override if the provider sends server-to-server callbacks.
123
141
  Returns None if the event is not relevant.
142
+
143
+ Implementations MUST authenticate the event before returning a
144
+ result (e.g. verify a signature or shared secret) and raise
145
+ :class:`WebhookVerificationError` when verification fails or the
146
+ required verification secret is not configured. Unverified
147
+ payloads must never produce a state-changing result.
124
148
  """
125
149
  return None
126
150
 
@@ -0,0 +1,84 @@
1
+ """
2
+ Checkout authorization for the payment flow.
3
+
4
+ Payment endpoints act on a :class:`~django_forms_workflows.models.FormSubmission`
5
+ that is in ``pending_payment``. Access is governed by exactly one policy:
6
+
7
+ * **Owned submissions** — only the authenticated submitter may act on them.
8
+ A numeric submission or payment-record ID alone grants nothing.
9
+ * **Anonymous (public-form) submissions** — the browser that just posted the
10
+ public form is issued a *checkout credential*: an unguessable token stored
11
+ in its session and bound to the submission ID, valid for a short time
12
+ (``FORMS_WORKFLOWS_PAYMENT_CHECKOUT_TTL`` seconds, default one hour).
13
+
14
+ The credential is granted by the submit views when a submission enters
15
+ ``pending_payment`` and is revoked when the checkout is cancelled. It is
16
+ never exposed in a URL. Callers that fail the policy receive a
17
+ non-disclosing 404, identical to a nonexistent submission.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import secrets
23
+ import time
24
+
25
+ from django.conf import settings
26
+
27
+ SESSION_KEY = "payment_checkouts"
28
+ DEFAULT_CHECKOUT_TTL_SECONDS = 3600 # 1 hour
29
+
30
+
31
+ def _checkout_ttl() -> int:
32
+ return int(
33
+ getattr(
34
+ settings,
35
+ "FORMS_WORKFLOWS_PAYMENT_CHECKOUT_TTL",
36
+ DEFAULT_CHECKOUT_TTL_SECONDS,
37
+ )
38
+ )
39
+
40
+
41
+ def grant_checkout_access(request, submission) -> None:
42
+ """Bind a short-lived checkout credential for *submission* to the session.
43
+
44
+ Called by the submit views right after a submission enters
45
+ ``pending_payment`` so the same browser session can complete (confirm,
46
+ return, or cancel) the checkout that belongs to it.
47
+ """
48
+ checkouts = dict(request.session.get(SESSION_KEY, {}))
49
+ checkouts[str(submission.id)] = {
50
+ "token": secrets.token_urlsafe(32),
51
+ "exp": time.time() + _checkout_ttl(),
52
+ }
53
+ request.session[SESSION_KEY] = checkouts
54
+ request.session.modified = True
55
+
56
+
57
+ def revoke_checkout_access(request, submission) -> None:
58
+ """Drop any checkout credential for *submission* from the session."""
59
+ checkouts = dict(request.session.get(SESSION_KEY, {}))
60
+ if checkouts.pop(str(submission.id), None) is not None:
61
+ request.session[SESSION_KEY] = checkouts
62
+ request.session.modified = True
63
+
64
+
65
+ def has_checkout_access(request, submission) -> bool:
66
+ """The single payment access policy used by every payment endpoint.
67
+
68
+ * Authenticated submitter: may act only on their own submission.
69
+ * Anonymous submission (``submitter`` is null): the caller's session
70
+ must hold an unexpired checkout credential issued by
71
+ :func:`grant_checkout_access`.
72
+ """
73
+ if submission.submitter_id is not None:
74
+ return (
75
+ request.user.is_authenticated and request.user.pk == submission.submitter_id
76
+ )
77
+
78
+ entry = request.session.get(SESSION_KEY, {}).get(str(submission.id))
79
+ if not entry:
80
+ return False
81
+ try:
82
+ return float(entry.get("exp", 0)) > time.time()
83
+ except (TypeError, ValueError):
84
+ return False