django-forms-workflows 0.72.4__tar.gz → 0.74.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 (219) hide show
  1. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/CHANGELOG.md +67 -0
  2. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/PKG-INFO +1 -1
  3. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/admin.py +14 -1
  4. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/diff_views.py +32 -1
  5. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/form_builder_views.py +8 -0
  6. django_forms_workflows-0.74.0/django_forms_workflows/migrations/0094_formfield_show_help_text_in_detail.py +24 -0
  7. django_forms_workflows-0.74.0/django_forms_workflows/migrations/0095_notificationrule_cc_fields.py +35 -0
  8. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/models.py +23 -0
  9. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/static/django_forms_workflows/js/form-builder.js +10 -0
  10. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/sync_api.py +5 -0
  11. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/tasks.py +104 -7
  12. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/_form_data_rows.html +2 -0
  13. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/submission_detail.html +7 -0
  14. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/emails/approval_notification.html +1 -0
  15. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/views.py +5 -0
  16. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/pyproject.toml +1 -1
  17. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/LICENSE +0 -0
  18. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/README.md +0 -0
  19. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/__init__.py +0 -0
  20. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/api_urls.py +0 -0
  21. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/api_views.py +0 -0
  22. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/apps.py +0 -0
  23. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/callback_registry.py +0 -0
  24. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/conditions.py +0 -0
  25. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/context_processors.py +0 -0
  26. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/data_sources/__init__.py +0 -0
  27. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/data_sources/base.py +0 -0
  28. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/data_sources/database_source.py +0 -0
  29. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/data_sources/ldap_source.py +0 -0
  30. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/data_sources/user_source.py +0 -0
  31. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/email_backends/__init__.py +0 -0
  32. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/email_backends/gmail_api.py +0 -0
  33. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/form_builder_urls.py +0 -0
  34. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/forms.py +0 -0
  35. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/handlers/__init__.py +0 -0
  36. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/handlers/api_handler.py +0 -0
  37. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/handlers/base.py +0 -0
  38. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/handlers/database_handler.py +0 -0
  39. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/handlers/email_handler.py +0 -0
  40. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/handlers/executor.py +0 -0
  41. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/handlers/file_handler.py +0 -0
  42. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/handlers/ldap_handler.py +0 -0
  43. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/history.py +0 -0
  44. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/ldap_backend.py +0 -0
  45. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/management/commands/create_default_templates.py +0 -0
  46. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/management/commands/pull_forms.py +0 -0
  47. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/management/commands/push_forms.py +0 -0
  48. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/management/commands/seed_farm_demo.py +0 -0
  49. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/management/commands/seed_pcn_form.py +0 -0
  50. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/management/commands/seed_prefill_sources.py +0 -0
  51. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/management/commands/sync_ldap_profiles.py +0 -0
  52. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/management/commands/sync_ldap_users.py +0 -0
  53. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/management/commands/test_db_connection.py +0 -0
  54. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0001_initial.py +0 -0
  55. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0002_prefillsource_alter_formfield_prefill_source_and_more.py +0 -0
  56. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0003_postsubmissionaction.py +0 -0
  57. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0004_formtemplate.py +0 -0
  58. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0005_advanced_conditional_logic.py +0 -0
  59. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0006_workflowdefinition_visual_workflow_data_and_more.py +0 -0
  60. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0007_add_userprofile_ldap_enhancements.py +0 -0
  61. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0008_add_file_workflow_models.py +0 -0
  62. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0009_add_formfield_readonly.py +0 -0
  63. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0010_add_prefillsource_template_fields.py +0 -0
  64. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0011_postsubmissionaction_email_body_template_and_more.py +0 -0
  65. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0012_add_approval_step_fields.py +0 -0
  66. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0013_add_database_query_key.py +0 -0
  67. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0014_add_form_category.py +0 -0
  68. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0015_add_workflowstage_staged_approvals.py +0 -0
  69. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0016_add_pdf_generation.py +0 -0
  70. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0017_add_notification_batching.py +0 -0
  71. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0018_add_form_category_parent.py +0 -0
  72. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0019_add_ldap_group_profile.py +0 -0
  73. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0020_add_bulk_export_field.py +0 -0
  74. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0021_alter_pendingnotification_scheduled_for_and_more.py +0 -0
  75. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0022_add_bulk_pdf_export.py +0 -0
  76. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0023_add_performance_indexes.py +0 -0
  77. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0024_add_allow_resubmit.py +0 -0
  78. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0025_workflowstage_approve_label.py +0 -0
  79. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0026_alter_formfield_width.py +0 -0
  80. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0027_formsubmission_form_data_gin.py +0 -0
  81. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0028_workflowstagegroupconfig.py +0 -0
  82. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0029_formfield_workflow_stage_fk_drop_groupconfig.py +0 -0
  83. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0030_formfield_phone_type.py +0 -0
  84. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0031_sub_workflows.py +0 -0
  85. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0032_approved_pending_detached_sub_workflow.py +0 -0
  86. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0033_add_reject_parent_to_subworkflowdefinition.py +0 -0
  87. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0034_add_notificationlog.py +0 -0
  88. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0035_migrate_prefill_source_strings.py +0 -0
  89. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0036_add_workflowstage_auto_created.py +0 -0
  90. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0037_migrate_flat_workflows_to_staged.py +0 -0
  91. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0038_remove_legacy_prefill_source_and_approval_step.py +0 -0
  92. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0039_update_help_text.py +0 -0
  93. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0040_migrate_deprecated_features.py +0 -0
  94. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0041_remove_deprecated_fields.py +0 -0
  95. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0042_add_sub_workflow_section_label.py +0 -0
  96. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0043_fix_form_data_decimal_encoding.py +0 -0
  97. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0044_remove_subworkflowinstance_label.py +0 -0
  98. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0045_stageapprovalgroup_workflow_name_label.py +0 -0
  99. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0046_workflow_definition_one_to_many.py +0 -0
  100. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0047_add_hide_approval_history.py +0 -0
  101. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0048_workflowdefinition_trigger_conditions_and_more.py +0 -0
  102. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0049_formfield_multifile_type.py +0 -0
  103. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0050_workflowdefinition_collapse_parallel_stages.py +0 -0
  104. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0051_formdefinition_is_listed.py +0 -0
  105. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0052_dynamic_assignee_and_form_field_notifications.py +0 -0
  106. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0053_stageformfieldnotification_static_emails.py +0 -0
  107. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0054_rename_approved_pending_to_pending_approval.py +0 -0
  108. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0055_add_send_back.py +0 -0
  109. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0056_calculated_formula_spreadsheet.py +0 -0
  110. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0057_add_currency_field_type.py +0 -0
  111. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0058_rename_assignee_email_field_add_lookup_type.py +0 -0
  112. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0059_add_multiselect_list_field_type.py +0 -0
  113. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0060_alter_workflowstage_assignee_lookup_type_helptext.py +0 -0
  114. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0061_add_country_us_state_field_types.py +0 -0
  115. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0062_add_batch_import_to_formdefinition.py +0 -0
  116. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0063_formdefinition_api_enabled_apitoken.py +0 -0
  117. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0064_add_reviewer_groups.py +0 -0
  118. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0065_workflownotification.py +0 -0
  119. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0066_add_notify_submitter_to_workflownotification.py +0 -0
  120. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0067_migrate_legacy_notify_flags.py +0 -0
  121. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0068_drop_legacy_notify_fields.py +0 -0
  122. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0069_add_signature_field_type.py +0 -0
  123. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0070_workflowstage_allow_edit_form_data.py +0 -0
  124. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0071_alter_pendingnotification_notification_type.py +0 -0
  125. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0072_add_change_history.py +0 -0
  126. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0073_add_stage_notify_assignee_on_final_decision.py +0 -0
  127. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0074_add_notification_rule.py +0 -0
  128. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0075_migrate_to_notification_rules.py +0 -0
  129. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0076_update_notification_event_choices.py +0 -0
  130. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0077_drop_legacy_notification_models.py +0 -0
  131. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0078_allow_anonymous_submissions.py +0 -0
  132. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0079_webhookendpoint_webhookdeliverylog.py +0 -0
  133. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0080_add_rating_matrix_address_slider_fields_and_form_controls.py +0 -0
  134. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0081_add_success_page_fields.py +0 -0
  135. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0082_add_document_template_model.py +0 -0
  136. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0083_workflowdefinition_start_trigger.py +0 -0
  137. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0084_add_shared_option_list.py +0 -0
  138. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0085_add_payment_system.py +0 -0
  139. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0086_add_embed_enabled.py +0 -0
  140. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0087_notificationrule_use_triggering_stage.py +0 -0
  141. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0088_notificationrule_body_template_and_events.py +0 -0
  142. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0089_workflowstage_hide_comment_field.py +0 -0
  143. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0090_alter_stageapprovalgroup_options_and_more.py +0 -0
  144. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0091_add_uuid_fields.py +0 -0
  145. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0092_add_user_notification_preference.py +0 -0
  146. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/0093_normalize_conditional_operators.py +0 -0
  147. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/migrations/__init__.py +0 -0
  148. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/payments/__init__.py +0 -0
  149. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/payments/base.py +0 -0
  150. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/payments/registry.py +0 -0
  151. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/payments/stripe_provider.py +0 -0
  152. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/payments/views.py +0 -0
  153. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/reporting_views.py +0 -0
  154. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/signals.py +0 -0
  155. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/sso_backends.py +0 -0
  156. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/sso_urls.py +0 -0
  157. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/sso_views.py +0 -0
  158. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/static/django_forms_workflows/api/swagger-ui-bundle.js +0 -0
  159. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/static/django_forms_workflows/api/swagger-ui.css +0 -0
  160. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/static/django_forms_workflows/css/forms.css +0 -0
  161. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/static/django_forms_workflows/js/dfw-embed.js +0 -0
  162. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/static/django_forms_workflows/js/form-enhancements.js +0 -0
  163. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/static/django_forms_workflows/js/payment-stripe.js +0 -0
  164. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/static/django_forms_workflows/js/signature-pad.js +0 -0
  165. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/static/django_forms_workflows/js/workflow-builder.js +0 -0
  166. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/sync_views.py +0 -0
  167. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/admin/django_forms_workflows/_sync_diff.html +0 -0
  168. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/admin/django_forms_workflows/diff_forms.html +0 -0
  169. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/admin/django_forms_workflows/form_builder.html +0 -0
  170. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/admin/django_forms_workflows/formdef_change_form.html +0 -0
  171. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/admin/django_forms_workflows/formdefinition/change_list.html +0 -0
  172. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_import.html +0 -0
  173. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_pull.html +0 -0
  174. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_push.html +0 -0
  175. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/admin/django_forms_workflows/workflow_builder.html +0 -0
  176. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/_category_node.html +0 -0
  177. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/_field_value.html +0 -0
  178. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/analytics_dashboard.html +0 -0
  179. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/api/docs.html +0 -0
  180. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/approval_inbox.html +0 -0
  181. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/approve.html +0 -0
  182. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/base.html +0 -0
  183. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/batch_import_result.html +0 -0
  184. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/completed_approvals.html +0 -0
  185. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/discard_draft_confirm.html +0 -0
  186. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/embed_base.html +0 -0
  187. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/embed_success.html +0 -0
  188. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/form_categories.html +0 -0
  189. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/form_embed.html +0 -0
  190. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/form_list.html +0 -0
  191. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/form_submit.html +0 -0
  192. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/my_submissions.html +0 -0
  193. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/notification_preferences.html +0 -0
  194. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/payment_collect.html +0 -0
  195. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/payment_error.html +0 -0
  196. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/public_submission_confirmation.html +0 -0
  197. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/rate_limited.html +0 -0
  198. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/reassign_task.html +0 -0
  199. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/resubmit_confirm.html +0 -0
  200. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/sso/login.html +0 -0
  201. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/sub_workflow_detail.html +0 -0
  202. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/submission_bulk_pdf.html +0 -0
  203. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/submission_pdf.html +0 -0
  204. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/submission_success.html +0 -0
  205. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/django_forms_workflows/withdraw_confirm.html +0 -0
  206. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/emails/approval_reminder.html +0 -0
  207. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/emails/approval_request.html +0 -0
  208. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/emails/email_styles.html +0 -0
  209. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/emails/escalation_notification.html +0 -0
  210. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/emails/notification_digest.html +0 -0
  211. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/emails/rejection_notification.html +0 -0
  212. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/emails/submission_notification.html +0 -0
  213. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templates/registration/login.html +0 -0
  214. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templatetags/__init__.py +0 -0
  215. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/templatetags/forms_workflows_tags.py +0 -0
  216. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/urls.py +0 -0
  217. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/utils.py +0 -0
  218. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/workflow_builder_views.py +0 -0
  219. {django_forms_workflows-0.72.4 → django_forms_workflows-0.74.0}/django_forms_workflows/workflow_engine.py +0 -0
@@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.74.0] - 2026-04-24
11
+
12
+ ### Fixed
13
+ - **Public decision comments now reach the submitter on approval too.**
14
+ The rejection email (`rejection_notification.html`) has always
15
+ rendered the approver's public decision comment, but the mirror
16
+ approval email (`approval_notification.html`) silently dropped it —
17
+ and `submission_detail.html`'s hidden-history mode (used when a
18
+ workflow sets `hide_approval_history=True`) also only surfaced the
19
+ comment on rejections. So an approved submission whose approver left
20
+ a "Welcome aboard / here's your next step" public note had no way to
21
+ reach the submitter. Both the approval email template and the
22
+ approved branch of the privacy-mode submission detail now render
23
+ `task.comments` exactly like the rejection side does. This makes
24
+ public-comment behaviour symmetrical on both outcomes; the
25
+ `hide_approval_history` flag remains purely a "hide the other
26
+ approval stages' activity" toggle.
27
+
28
+ ### Added
29
+ - **`reviewer_groups` now round-trips through sync push/pull.** The
30
+ `FormDefinition.reviewer_groups` M2M was silently dropped by the
31
+ export/import path, so pulling a form into another environment lost
32
+ every non-admin reviewer assignment. The sync serializer and importer
33
+ now treat `reviewer_groups` exactly like `submit_groups` /
34
+ `view_groups` / `admin_groups` (resolved by group name, auto-created
35
+ on import). The admin diff viewer also reports reviewer_group
36
+ additions and removals.
37
+ - **Full `PostSubmissionAction` coverage in the diff viewer.** The
38
+ admin side-by-side diff only compared a small subset of action fields
39
+ (primarily email_to / api_endpoint / conditions). It now mirrors the
40
+ sync serializer and flags changes to every round-tripped field:
41
+ database action config (`db_alias`, `db_schema`, `db_table`,
42
+ `db_lookup_field`, `db_user_field`, `db_field_mappings`), LDAP config
43
+ (`ldap_dn_template`, `ldap_field_mappings`), API config (`api_headers`,
44
+ `api_body_template`), custom handler config (`custom_handler_path`,
45
+ `custom_handler_config`), email config (`email_to_field`, `email_cc`,
46
+ `email_cc_field`, `email_body_template`, `email_template_name`),
47
+ `is_locked`, `description`, and retry/failure settings
48
+ (`fail_silently`, `retry_on_failure`, `max_retries`).
49
+
50
+ - **CC recipients on notification rules (static and dynamic).** Two new
51
+ fields on `NotificationRule`:
52
+ - `cc_static_emails` — comma-separated fixed CC addresses.
53
+ - `cc_email_field` — form field slug whose submitted value is used as
54
+ a dynamic CC address, resolved per-submission.
55
+ The TO list still drives one personalised email per recipient (so
56
+ `{{ approver }}` context and per-user muting keep working); CC
57
+ addresses are attached to the first outgoing email of each rule
58
+ execution, so each CC recipient receives exactly one copy. CC
59
+ addresses that duplicate a TO are dropped, and per-user mute
60
+ preferences apply to CCs as well. For batched/digest workflows CCs
61
+ are queued alongside TOs. Admin UI (inline + standalone) and
62
+ `NotificationLog` (prefixed `cc:` on CC rows) updated accordingly.
63
+ New migration `0095_notificationrule_cc_fields`.
64
+
65
+ ## [0.73.0] - 2026-04-23
66
+
67
+ ### Added
68
+ - **Per-field `show_help_text_in_detail` flag.** Fields can now opt into
69
+ rendering their `help_text` alongside the stored value in the
70
+ submission and approval detail views. Intended for attestation /
71
+ consent statements attached to initials or signature fields, where
72
+ the statement the user agreed to should be visible with their
73
+ initials rather than only at fill-in time. Exposed in the form
74
+ builder (checkbox under Help Text) and in the Django admin. New
75
+ migration `0094_formfield_show_help_text_in_detail`.
76
+
10
77
  ## [0.72.4] - 2026-04-23
11
78
 
12
79
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-forms-workflows
3
- Version: 0.72.4
3
+ Version: 0.74.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
@@ -148,6 +148,7 @@ class FormFieldInline(nested_admin.NestedStackedInline):
148
148
  ("order", "field_label", "field_name", "field_type"),
149
149
  ("required", "readonly", "workflow_stage"),
150
150
  ("help_text", "placeholder", "width", "css_class"),
151
+ ("show_help_text_in_detail",),
151
152
  )
152
153
  },
153
154
  ),
@@ -342,7 +343,14 @@ class FormFieldAdmin(admin.ModelAdmin):
342
343
  ),
343
344
  (
344
345
  "Display",
345
- {"fields": ("help_text", "placeholder"), "classes": ("collapse",)},
346
+ {
347
+ "fields": (
348
+ "help_text",
349
+ "show_help_text_in_detail",
350
+ "placeholder",
351
+ ),
352
+ "classes": ("collapse",),
353
+ },
346
354
  ),
347
355
  (
348
356
  "Conditional Display",
@@ -560,6 +568,7 @@ class NotificationRuleInline(nested_admin.NestedStackedInline):
560
568
  "notify_stage_groups",
561
569
  ),
562
570
  ("email_field", "static_emails"),
571
+ ("cc_email_field", "cc_static_emails"),
563
572
  "notify_groups",
564
573
  "subject_template",
565
574
  )
@@ -918,6 +927,7 @@ class FormDefinitionAdmin(nested_admin.NestedModelAdmin):
918
927
  field_type=field.field_type,
919
928
  required=field.required,
920
929
  help_text=field.help_text,
930
+ show_help_text_in_detail=field.show_help_text_in_detail,
921
931
  placeholder=field.placeholder,
922
932
  width=field.width,
923
933
  css_class=field.css_class,
@@ -1781,6 +1791,8 @@ class NotificationRuleAdmin(admin.ModelAdmin):
1781
1791
  "stage__name",
1782
1792
  "email_field",
1783
1793
  "static_emails",
1794
+ "cc_email_field",
1795
+ "cc_static_emails",
1784
1796
  )
1785
1797
  autocomplete_fields = ("workflow",)
1786
1798
  fieldsets = (
@@ -1796,6 +1808,7 @@ class NotificationRuleAdmin(admin.ModelAdmin):
1796
1808
  "notify_stage_groups",
1797
1809
  ),
1798
1810
  ("email_field", "static_emails"),
1811
+ ("cc_email_field", "cc_static_emails"),
1799
1812
  "notify_groups",
1800
1813
  "subject_template",
1801
1814
  )
@@ -183,7 +183,7 @@ def _build_summary(forms_data):
183
183
  diffs.append(f"category: {b_cat_slug!r} → {o_cat_slug!r}")
184
184
 
185
185
  # Permission groups
186
- for g in ("submit_groups", "view_groups", "admin_groups"):
186
+ for g in ("submit_groups", "view_groups", "admin_groups", "reviewer_groups"):
187
187
  bg = set(b_form.get(g, []))
188
188
  og = set(o_form.get(g, []))
189
189
  if bg != og:
@@ -216,18 +216,49 @@ def _build_summary(forms_data):
216
216
  )
217
217
  b_action_map = {a.get("name"): a for a in b_actions}
218
218
  o_action_map = {a.get("name"): a for a in o_actions}
219
+ # Keep in sync with sync_api._serialize_post_action so every
220
+ # field that round-trips through push/pull also shows up here.
219
221
  action_check_keys = [
220
222
  "action_type",
221
223
  "trigger",
222
224
  "is_active",
223
225
  "order",
226
+ "is_locked",
227
+ "description",
228
+ # Database action
229
+ "db_alias",
230
+ "db_schema",
231
+ "db_table",
232
+ "db_lookup_field",
233
+ "db_user_field",
234
+ "db_field_mappings",
235
+ # LDAP action
236
+ "ldap_dn_template",
237
+ "ldap_field_mappings",
238
+ # API action
224
239
  "api_endpoint",
225
240
  "api_method",
241
+ "api_headers",
242
+ "api_body_template",
243
+ # Custom handler
244
+ "custom_handler_path",
245
+ "custom_handler_config",
246
+ # Email action
226
247
  "email_to",
248
+ "email_to_field",
249
+ "email_cc",
250
+ "email_cc_field",
227
251
  "email_subject_template",
252
+ "email_body_template",
253
+ "email_template_name",
254
+ # Conditional execution
228
255
  "condition_field",
229
256
  "condition_operator",
230
257
  "condition_value",
258
+ # Retry / failure handling
259
+ "fail_silently",
260
+ "retry_on_failure",
261
+ "max_retries",
231
262
  ]
232
263
  for name in sorted(b_action_names & o_action_names):
233
264
  ba, oa = b_action_map[name], o_action_map[name]
@@ -132,6 +132,7 @@ def form_builder_clone(request, form_id):
132
132
  field_type=field.field_type,
133
133
  required=field.required,
134
134
  help_text=field.help_text,
135
+ show_help_text_in_detail=field.show_help_text_in_detail,
135
136
  placeholder=field.placeholder,
136
137
  width=field.width,
137
138
  css_class=field.css_class,
@@ -294,6 +295,7 @@ def form_builder_load(request, form_id):
294
295
  "field_type": field.field_type,
295
296
  "required": field.required,
296
297
  "help_text": field.help_text or "",
298
+ "show_help_text_in_detail": field.show_help_text_in_detail,
297
299
  "placeholder": field.placeholder or "",
298
300
  "width": field.width,
299
301
  "css_class": field.css_class or "",
@@ -498,6 +500,9 @@ def form_builder_save(request):
498
500
  "field_type": field_data.get("field_type", "text"),
499
501
  "required": field_data.get("required", False),
500
502
  "help_text": field_data.get("help_text", ""),
503
+ "show_help_text_in_detail": field_data.get(
504
+ "show_help_text_in_detail", False
505
+ ),
501
506
  "placeholder": field_data.get("placeholder", ""),
502
507
  "width": field_data.get("width", "full"),
503
508
  "css_class": field_data.get("css_class", ""),
@@ -652,6 +657,9 @@ def form_builder_preview(request):
652
657
  "field_type": field_data.get("field_type", "text"),
653
658
  "required": field_data.get("required", False),
654
659
  "help_text": field_data.get("help_text", ""),
660
+ "show_help_text_in_detail": field_data.get(
661
+ "show_help_text_in_detail", False
662
+ ),
655
663
  "placeholder": field_data.get("placeholder", ""),
656
664
  "width": field_data.get("width", "full"),
657
665
  "css_class": field_data.get("css_class", ""),
@@ -0,0 +1,24 @@
1
+ from django.db import migrations, models
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+
6
+ dependencies = [
7
+ ("django_forms_workflows", "0093_normalize_conditional_operators"),
8
+ ]
9
+
10
+ operations = [
11
+ migrations.AddField(
12
+ model_name="formfield",
13
+ name="show_help_text_in_detail",
14
+ field=models.BooleanField(
15
+ default=False,
16
+ help_text=(
17
+ "If checked, the field's help text is shown next to the value "
18
+ "in the submission and approval detail view. Use for "
19
+ "attestation or consent statements attached to "
20
+ "initials/signature fields."
21
+ ),
22
+ ),
23
+ ),
24
+ ]
@@ -0,0 +1,35 @@
1
+ from django.db import migrations, models
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+
6
+ dependencies = [
7
+ ("django_forms_workflows", "0094_formfield_show_help_text_in_detail"),
8
+ ]
9
+
10
+ operations = [
11
+ migrations.AddField(
12
+ model_name="notificationrule",
13
+ name="cc_email_field",
14
+ field=models.CharField(
15
+ blank=True,
16
+ default="",
17
+ help_text=(
18
+ "Form field slug whose submitted value is a CC email "
19
+ "address. Resolved from form_data at send time "
20
+ "(varies per submission)."
21
+ ),
22
+ max_length=200,
23
+ ),
24
+ ),
25
+ migrations.AddField(
26
+ model_name="notificationrule",
27
+ name="cc_static_emails",
28
+ field=models.CharField(
29
+ blank=True,
30
+ default="",
31
+ help_text="Comma-separated fixed CC email addresses.",
32
+ max_length=1000,
33
+ ),
34
+ ),
35
+ ]
@@ -597,6 +597,14 @@ class FormField(models.Model):
597
597
  # Display
598
598
  order = models.IntegerField(default=0, help_text="Fields are sorted by this number")
599
599
  help_text = models.TextField(blank=True, help_text="Instructions shown below field")
600
+ show_help_text_in_detail = models.BooleanField(
601
+ default=False,
602
+ help_text=(
603
+ "If checked, the field's help text is shown next to the value in the "
604
+ "submission and approval detail view. Use for attestation or consent "
605
+ "statements attached to initials/signature fields."
606
+ ),
607
+ )
600
608
  placeholder = models.CharField(max_length=200, blank=True)
601
609
  css_class = models.CharField(
602
610
  max_length=100, blank=True, help_text="CSS classes for styling"
@@ -1490,6 +1498,21 @@ class NotificationRule(models.Model):
1490
1498
  default="",
1491
1499
  help_text="Comma-separated fixed email addresses.",
1492
1500
  )
1501
+ cc_email_field = models.CharField(
1502
+ max_length=200,
1503
+ blank=True,
1504
+ default="",
1505
+ help_text=(
1506
+ "Form field slug whose submitted value is a CC email address. "
1507
+ "Resolved from form_data at send time (varies per submission)."
1508
+ ),
1509
+ )
1510
+ cc_static_emails = models.CharField(
1511
+ max_length=1000,
1512
+ blank=True,
1513
+ default="",
1514
+ help_text="Comma-separated fixed CC email addresses.",
1515
+ )
1493
1516
  notify_stage_assignees = models.BooleanField(
1494
1517
  default=False,
1495
1518
  help_text=(
@@ -366,6 +366,7 @@ class FormBuilder {
366
366
  field_type: fieldType,
367
367
  required: false,
368
368
  help_text: '',
369
+ show_help_text_in_detail: false,
369
370
  placeholder: '',
370
371
  width: 'full',
371
372
  css_class: '',
@@ -794,6 +795,12 @@ class FormBuilder {
794
795
  <div class="col-12">
795
796
  <label class="form-label">Help Text</label>
796
797
  <input type="text" class="form-control" id="propHelpText" value="${this.escapeHtml(field.help_text)}">
798
+ <div class="form-check mt-1">
799
+ <input class="form-check-input" type="checkbox" id="propShowHelpTextInDetail" ${field.show_help_text_in_detail ? 'checked' : ''}>
800
+ <label class="form-check-label small text-muted" for="propShowHelpTextInDetail">
801
+ Also show this help text next to the value in the submission/approval detail view (use for attestation / consent statements on initials fields).
802
+ </label>
803
+ </div>
797
804
  </div>
798
805
  <div class="col-12">
799
806
  <label class="form-label">Placeholder</label>
@@ -1393,6 +1400,7 @@ class FormBuilder {
1393
1400
  field_label: this.getDefaultLabel(fieldType),
1394
1401
  required: false,
1395
1402
  help_text: '',
1403
+ show_help_text_in_detail: false,
1396
1404
  placeholder: '',
1397
1405
  choices: '',
1398
1406
  width: 'full',
@@ -1670,6 +1678,8 @@ class FormBuilder {
1670
1678
  field.field_name = document.getElementById('propFieldName').value;
1671
1679
  field.required = document.getElementById('propRequired').checked;
1672
1680
  field.help_text = document.getElementById('propHelpText').value;
1681
+ const showHelpCheckbox = document.getElementById('propShowHelpTextInDetail');
1682
+ field.show_help_text_in_detail = showHelpCheckbox ? showHelpCheckbox.checked : false;
1673
1683
  field.placeholder = document.getElementById('propPlaceholder').value;
1674
1684
  field.width = document.getElementById('propWidth').value;
1675
1685
  field.css_class = document.getElementById('propCssClass').value;
@@ -445,6 +445,7 @@ def serialize_form(form_definition):
445
445
  "submit_groups": _group_names(form_definition.submit_groups),
446
446
  "view_groups": _group_names(form_definition.view_groups),
447
447
  "admin_groups": _group_names(form_definition.admin_groups),
448
+ "reviewer_groups": _group_names(form_definition.reviewer_groups),
448
449
  "allow_save_draft": form_definition.allow_save_draft,
449
450
  "allow_withdrawal": form_definition.allow_withdrawal,
450
451
  "allow_resubmit": form_definition.allow_resubmit,
@@ -543,6 +544,7 @@ def build_export_payload(queryset):
543
544
  "submit_groups",
544
545
  "view_groups",
545
546
  "admin_groups",
547
+ "reviewer_groups",
546
548
  "category",
547
549
  "category__allowed_groups",
548
550
  )
@@ -980,6 +982,9 @@ def import_form(form_data, conflict="update", category_cache=None):
980
982
  form_obj.admin_groups.set(
981
983
  [_get_or_create_group(n) for n in fd.get("admin_groups", [])]
982
984
  )
985
+ form_obj.reviewer_groups.set(
986
+ [_get_or_create_group(n) for n in fd.get("reviewer_groups", [])]
987
+ )
983
988
 
984
989
  # ── Workflows (created BEFORE fields so stage FKs can be resolved) ─────────
985
990
  wf_data = form_data.get("workflow")
@@ -99,8 +99,10 @@ def _send_html_email(
99
99
  *,
100
100
  notification_type: str = "other",
101
101
  submission_id: int | None = None,
102
+ cc: Iterable[str] | None = None,
102
103
  ) -> None:
103
104
  to_list = [e for e in to if e]
105
+ cc_list = [e for e in (cc or []) if e and e not in to_list]
104
106
  if not to_list:
105
107
  logger.info("Skipping email '%s' (no recipients)", subject)
106
108
  _write_notification_log(
@@ -122,12 +124,21 @@ def _send_html_email(
122
124
  )
123
125
 
124
126
  msg = EmailMultiAlternatives(
125
- subject=subject, body=text_body, from_email=from_addr, to=to_list
127
+ subject=subject,
128
+ body=text_body,
129
+ from_email=from_addr,
130
+ to=to_list,
131
+ cc=cc_list or None,
126
132
  )
127
133
  msg.attach_alternative(html_body, "text/html")
128
134
  try:
129
135
  msg.send(fail_silently=False)
130
- logger.info("Sent email '%s' to %s", subject, to_list)
136
+ logger.info(
137
+ "Sent email '%s' to %s%s",
138
+ subject,
139
+ to_list,
140
+ f" cc={cc_list}" if cc_list else "",
141
+ )
131
142
  for recipient in to_list:
132
143
  _write_notification_log(
133
144
  notification_type=notification_type,
@@ -136,6 +147,14 @@ def _send_html_email(
136
147
  subject=subject,
137
148
  status="sent",
138
149
  )
150
+ for recipient in cc_list:
151
+ _write_notification_log(
152
+ notification_type=notification_type,
153
+ submission_id=submission_id,
154
+ recipient_email=f"cc:{recipient}",
155
+ subject=subject,
156
+ status="sent",
157
+ )
139
158
  except Exception as e: # pragma: no cover
140
159
  logger.exception("Failed sending email '%s' to %s: %s", subject, to_list, e)
141
160
  for recipient in to_list:
@@ -147,6 +166,15 @@ def _send_html_email(
147
166
  status="failed",
148
167
  error_message=str(e),
149
168
  )
169
+ for recipient in cc_list:
170
+ _write_notification_log(
171
+ notification_type=notification_type,
172
+ submission_id=submission_id,
173
+ recipient_email=f"cc:{recipient}",
174
+ subject=subject,
175
+ status="failed",
176
+ error_message=str(e),
177
+ )
150
178
 
151
179
 
152
180
  def _send_html_email_from_string(
@@ -158,6 +186,7 @@ def _send_html_email_from_string(
158
186
  *,
159
187
  notification_type: str = "other",
160
188
  submission_id: int | None = None,
189
+ cc: Iterable[str] | None = None,
161
190
  ) -> None:
162
191
  """Like _send_html_email but renders an inline Django template string
163
192
  instead of loading a template file. Used when a NotificationRule has a
@@ -166,6 +195,7 @@ def _send_html_email_from_string(
166
195
  from django.template import Context, Template
167
196
 
168
197
  to_list = [e for e in to if e]
198
+ cc_list = [e for e in (cc or []) if e and e not in to_list]
169
199
  if not to_list:
170
200
  logger.info("Skipping email '%s' (no recipients)", subject)
171
201
  _write_notification_log(
@@ -187,12 +217,21 @@ def _send_html_email_from_string(
187
217
  )
188
218
 
189
219
  msg = EmailMultiAlternatives(
190
- subject=subject, body=text_body, from_email=from_addr, to=to_list
220
+ subject=subject,
221
+ body=text_body,
222
+ from_email=from_addr,
223
+ to=to_list,
224
+ cc=cc_list or None,
191
225
  )
192
226
  msg.attach_alternative(html_body, "text/html")
193
227
  try:
194
228
  msg.send(fail_silently=False)
195
- logger.info("Sent email '%s' to %s", subject, to_list)
229
+ logger.info(
230
+ "Sent email '%s' to %s%s",
231
+ subject,
232
+ to_list,
233
+ f" cc={cc_list}" if cc_list else "",
234
+ )
196
235
  for recipient in to_list:
197
236
  _write_notification_log(
198
237
  notification_type=notification_type,
@@ -201,6 +240,14 @@ def _send_html_email_from_string(
201
240
  subject=subject,
202
241
  status="sent",
203
242
  )
243
+ for recipient in cc_list:
244
+ _write_notification_log(
245
+ notification_type=notification_type,
246
+ submission_id=submission_id,
247
+ recipient_email=f"cc:{recipient}",
248
+ subject=subject,
249
+ status="sent",
250
+ )
204
251
  except Exception as e: # pragma: no cover
205
252
  logger.exception("Failed sending email '%s' to %s: %s", subject, to_list, e)
206
253
  for recipient in to_list:
@@ -212,6 +259,15 @@ def _send_html_email_from_string(
212
259
  status="failed",
213
260
  error_message=str(e),
214
261
  )
262
+ for recipient in cc_list:
263
+ _write_notification_log(
264
+ notification_type=notification_type,
265
+ submission_id=submission_id,
266
+ recipient_email=f"cc:{recipient}",
267
+ subject=subject,
268
+ status="failed",
269
+ error_message=str(e),
270
+ )
215
271
 
216
272
 
217
273
  def _write_notification_log(
@@ -1167,6 +1223,31 @@ def _collect_notification_recipients(
1167
1223
  return recipients
1168
1224
 
1169
1225
 
1226
+ def _collect_notification_cc(notif, form_data: dict) -> list[str]:
1227
+ """Return the deduplicated list of CC emails for a notification rule.
1228
+
1229
+ Combines static and dynamic CC sources:
1230
+
1231
+ 1. ``cc_email_field`` → form_data[slug] (dynamic per submission)
1232
+ 2. ``cc_static_emails`` → comma-separated fixed addresses
1233
+ """
1234
+ cc: list[str] = []
1235
+
1236
+ def _add(email: str | None) -> None:
1237
+ if email and email not in cc:
1238
+ cc.append(email)
1239
+
1240
+ if getattr(notif, "cc_email_field", ""):
1241
+ _add(_get_form_field_email(form_data, notif.cc_email_field))
1242
+
1243
+ for addr in (getattr(notif, "cc_static_emails", "") or "").split(","):
1244
+ addr = addr.strip()
1245
+ if addr and "@" in addr:
1246
+ _add(addr)
1247
+
1248
+ return cc
1249
+
1250
+
1170
1251
  def _build_form_field_notification_context(
1171
1252
  submission: FormSubmission,
1172
1253
  task: ApprovalTask | None = None,
@@ -1304,6 +1385,9 @@ def send_notification_rules(
1304
1385
  recipients = _collect_notification_recipients(
1305
1386
  rule, form_data, submission=submission, task=task
1306
1387
  )
1388
+ cc_recipients = _collect_notification_cc(rule, form_data)
1389
+ # Prevent duplicate delivery: don't CC anyone already in the TO list.
1390
+ cc_recipients = [c for c in cc_recipients if c not in recipients]
1307
1391
  if not recipients:
1308
1392
  logger.info(
1309
1393
  "NotificationRule %s: no recipients resolved for submission %s; skipping.",
@@ -1321,7 +1405,7 @@ def send_notification_rules(
1321
1405
  .objects.filter(
1322
1406
  notification_preferences__rule_id=rule.id,
1323
1407
  notification_preferences__muted=True,
1324
- email__in=recipients,
1408
+ email__in=list(recipients) + list(cc_recipients),
1325
1409
  )
1326
1410
  .exclude(email="")
1327
1411
  .values_list("email", flat=True)
@@ -1329,6 +1413,7 @@ def send_notification_rules(
1329
1413
  if _muted_emails:
1330
1414
  before = len(recipients)
1331
1415
  recipients = [r for r in recipients if r not in _muted_emails]
1416
+ cc_recipients = [c for c in cc_recipients if c not in _muted_emails]
1332
1417
  logger.info(
1333
1418
  "NotificationRule %s: dropped %d muted recipient(s) for submission %s",
1334
1419
  rule.id,
@@ -1389,7 +1474,11 @@ def send_notification_rules(
1389
1474
  )
1390
1475
  if cadence != "immediate" and workflow is not None:
1391
1476
  scheduled_for = _compute_scheduled_for(workflow, submission)
1392
- for recipient in recipients:
1477
+ # In batched/digest mode we don't carry the TO/CC distinction —
1478
+ # each recipient just gets the summary email. Queue CCs alongside
1479
+ # TOs so they still receive the digest.
1480
+ queued_emails = list(recipients) + list(cc_recipients)
1481
+ for recipient in queued_emails:
1393
1482
  PendingNotification.objects.create(
1394
1483
  workflow=workflow,
1395
1484
  notification_type=event,
@@ -1400,7 +1489,7 @@ def send_notification_rules(
1400
1489
  )
1401
1490
  logger.info(
1402
1491
  "Queued %d %s notification(s) for submission %s (due %s)",
1403
- len(recipients),
1492
+ len(queued_emails),
1404
1493
  event,
1405
1494
  submission.id,
1406
1495
  scheduled_for,
@@ -1408,11 +1497,17 @@ def send_notification_rules(
1408
1497
  else:
1409
1498
  # Determine template: per-rule body_template overrides file.
1410
1499
  rule_body = getattr(rule, "body_template", "") or ""
1500
+ # Each TO recipient gets a personalized email (approver context).
1501
+ # CCs are attached to the first email only so CC recipients receive
1502
+ # exactly one copy per rule execution rather than one per TO.
1503
+ cc_pending = list(cc_recipients)
1411
1504
  for recipient in recipients:
1412
1505
  ctx = {**base_context}
1413
1506
  recipient_user = _users_by_email.get(recipient)
1414
1507
  if recipient_user:
1415
1508
  ctx["approver"] = recipient_user
1509
+ email_cc = cc_pending
1510
+ cc_pending = []
1416
1511
  if rule_body.strip():
1417
1512
  _send_html_email_from_string(
1418
1513
  subject,
@@ -1421,6 +1516,7 @@ def send_notification_rules(
1421
1516
  ctx,
1422
1517
  notification_type=event,
1423
1518
  submission_id=submission_id,
1519
+ cc=email_cc,
1424
1520
  )
1425
1521
  else:
1426
1522
  _send_html_email(
@@ -1430,4 +1526,5 @@ def send_notification_rules(
1430
1526
  ctx,
1431
1527
  notification_type=event,
1432
1528
  submission_id=submission_id,
1529
+ cc=email_cc,
1433
1530
  )
@@ -21,6 +21,7 @@
21
21
  {% for field in row.fields %}
22
22
  <div class="{% if row.type == 'pair' %}col-md-6{% elif row.type == 'triple' %}col-md-4{% else %}col-md-3{% endif %} px-3 py-2{% if not forloop.last %} border-end{% endif %}">
23
23
  <small class="text-muted fw-semibold">{{ field.label }}</small>
24
+ {% if field.help_text %}<div class="small text-secondary fst-italic mb-1">{{ field.help_text }}</div>{% endif %}
24
25
  <div>{% include "django_forms_workflows/_field_value.html" with entry=field %}</div>
25
26
  </div>
26
27
  {% endfor %}
@@ -32,6 +33,7 @@
32
33
  <tr>
33
34
  <td colspan="2" class="px-3 py-2">
34
35
  <small class="text-muted fw-semibold">{{ field.label }}</small>
36
+ {% if field.help_text %}<div class="small text-secondary fst-italic mb-1">{{ field.help_text }}</div>{% endif %}
35
37
  <div>{% include "django_forms_workflows/_field_value.html" with entry=field %}</div>
36
38
  </td>
37
39
  </tr>