django-forms-workflows 0.36.2__tar.gz → 0.37.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 (163) hide show
  1. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/CHANGELOG.md +34 -0
  2. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/PKG-INFO +1 -1
  3. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/admin.py +8 -1
  4. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/database_source.py +80 -0
  5. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/diff_views.py +4 -0
  6. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/forms.py +102 -9
  7. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/email_handler.py +12 -6
  8. django_forms_workflows-0.37.0/django_forms_workflows/migrations/0064_add_reviewer_groups.py +27 -0
  9. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/models.py +9 -0
  10. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sync_api.py +9 -0
  11. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/utils.py +6 -0
  12. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/views.py +57 -7
  13. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/pyproject.toml +1 -1
  14. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/LICENSE +0 -0
  15. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/README.md +0 -0
  16. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/__init__.py +0 -0
  17. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/api_urls.py +0 -0
  18. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/api_views.py +0 -0
  19. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/apps.py +0 -0
  20. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/conditions.py +0 -0
  21. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/context_processors.py +0 -0
  22. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/__init__.py +0 -0
  23. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/base.py +0 -0
  24. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/ldap_source.py +0 -0
  25. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/user_source.py +0 -0
  26. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/email_backends/__init__.py +0 -0
  27. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/email_backends/gmail_api.py +0 -0
  28. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/form_builder_urls.py +0 -0
  29. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/form_builder_views.py +0 -0
  30. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/__init__.py +0 -0
  31. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/api_handler.py +0 -0
  32. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/base.py +0 -0
  33. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/database_handler.py +0 -0
  34. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/executor.py +0 -0
  35. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/file_handler.py +0 -0
  36. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/ldap_handler.py +0 -0
  37. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/ldap_backend.py +0 -0
  38. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/create_default_templates.py +0 -0
  39. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/pull_forms.py +0 -0
  40. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/push_forms.py +0 -0
  41. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/seed_farm_demo.py +0 -0
  42. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/seed_pcn_form.py +0 -0
  43. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/seed_prefill_sources.py +0 -0
  44. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/sync_ldap_profiles.py +0 -0
  45. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/sync_ldap_users.py +0 -0
  46. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/test_db_connection.py +0 -0
  47. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0001_initial.py +0 -0
  48. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0002_prefillsource_alter_formfield_prefill_source_and_more.py +0 -0
  49. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0003_postsubmissionaction.py +0 -0
  50. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0004_formtemplate.py +0 -0
  51. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0005_advanced_conditional_logic.py +0 -0
  52. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0006_workflowdefinition_visual_workflow_data_and_more.py +0 -0
  53. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0007_add_userprofile_ldap_enhancements.py +0 -0
  54. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0008_add_file_workflow_models.py +0 -0
  55. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0009_add_formfield_readonly.py +0 -0
  56. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0010_add_prefillsource_template_fields.py +0 -0
  57. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0011_postsubmissionaction_email_body_template_and_more.py +0 -0
  58. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0012_add_approval_step_fields.py +0 -0
  59. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0013_add_database_query_key.py +0 -0
  60. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0014_add_form_category.py +0 -0
  61. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0015_add_workflowstage_staged_approvals.py +0 -0
  62. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0016_add_pdf_generation.py +0 -0
  63. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0017_add_notification_batching.py +0 -0
  64. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0018_add_form_category_parent.py +0 -0
  65. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0019_add_ldap_group_profile.py +0 -0
  66. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0020_add_bulk_export_field.py +0 -0
  67. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0021_alter_pendingnotification_scheduled_for_and_more.py +0 -0
  68. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0022_add_bulk_pdf_export.py +0 -0
  69. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0023_add_performance_indexes.py +0 -0
  70. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0024_add_allow_resubmit.py +0 -0
  71. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0025_workflowstage_approve_label.py +0 -0
  72. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0026_alter_formfield_width.py +0 -0
  73. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0027_formsubmission_form_data_gin.py +0 -0
  74. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0028_workflowstagegroupconfig.py +0 -0
  75. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0029_formfield_workflow_stage_fk_drop_groupconfig.py +0 -0
  76. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0030_formfield_phone_type.py +0 -0
  77. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0031_sub_workflows.py +0 -0
  78. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0032_approved_pending_detached_sub_workflow.py +0 -0
  79. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0033_add_reject_parent_to_subworkflowdefinition.py +0 -0
  80. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0034_add_notificationlog.py +0 -0
  81. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0035_migrate_prefill_source_strings.py +0 -0
  82. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0036_add_workflowstage_auto_created.py +0 -0
  83. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0037_migrate_flat_workflows_to_staged.py +0 -0
  84. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0038_remove_legacy_prefill_source_and_approval_step.py +0 -0
  85. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0039_update_help_text.py +0 -0
  86. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0040_migrate_deprecated_features.py +0 -0
  87. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0041_remove_deprecated_fields.py +0 -0
  88. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0042_add_sub_workflow_section_label.py +0 -0
  89. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0043_fix_form_data_decimal_encoding.py +0 -0
  90. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0044_remove_subworkflowinstance_label.py +0 -0
  91. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0045_stageapprovalgroup_workflow_name_label.py +0 -0
  92. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0046_workflow_definition_one_to_many.py +0 -0
  93. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0047_add_hide_approval_history.py +0 -0
  94. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0048_workflowdefinition_trigger_conditions_and_more.py +0 -0
  95. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0049_formfield_multifile_type.py +0 -0
  96. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0050_workflowdefinition_collapse_parallel_stages.py +0 -0
  97. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0051_formdefinition_is_listed.py +0 -0
  98. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0052_dynamic_assignee_and_form_field_notifications.py +0 -0
  99. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0053_stageformfieldnotification_static_emails.py +0 -0
  100. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0054_rename_approved_pending_to_pending_approval.py +0 -0
  101. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0055_add_send_back.py +0 -0
  102. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0056_calculated_formula_spreadsheet.py +0 -0
  103. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0057_add_currency_field_type.py +0 -0
  104. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0058_rename_assignee_email_field_add_lookup_type.py +0 -0
  105. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0059_add_multiselect_list_field_type.py +0 -0
  106. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0060_alter_workflowstage_assignee_lookup_type_helptext.py +0 -0
  107. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0061_add_country_us_state_field_types.py +0 -0
  108. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0062_add_batch_import_to_formdefinition.py +0 -0
  109. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0063_formdefinition_api_enabled_apitoken.py +0 -0
  110. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/__init__.py +0 -0
  111. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/signals.py +0 -0
  112. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sso_backends.py +0 -0
  113. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sso_urls.py +0 -0
  114. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sso_views.py +0 -0
  115. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/static/django_forms_workflows/api/swagger-ui-bundle.js +0 -0
  116. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/static/django_forms_workflows/api/swagger-ui.css +0 -0
  117. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/static/django_forms_workflows/js/form-builder.js +0 -0
  118. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/static/django_forms_workflows/js/form-enhancements.js +0 -0
  119. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/static/django_forms_workflows/js/workflow-builder.js +0 -0
  120. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sync_views.py +0 -0
  121. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/tasks.py +0 -0
  122. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/admin/django_forms_workflows/_sync_diff.html +0 -0
  123. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/admin/django_forms_workflows/diff_forms.html +0 -0
  124. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/admin/django_forms_workflows/form_builder.html +0 -0
  125. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/admin/django_forms_workflows/formdef_change_form.html +0 -0
  126. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/admin/django_forms_workflows/formdefinition/change_list.html +0 -0
  127. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_import.html +0 -0
  128. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_pull.html +0 -0
  129. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/admin/django_forms_workflows/sync_push.html +0 -0
  130. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/admin/django_forms_workflows/workflow_builder.html +0 -0
  131. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/_category_node.html +0 -0
  132. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/api/docs.html +0 -0
  133. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/approval_inbox.html +0 -0
  134. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/approve.html +0 -0
  135. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/base.html +0 -0
  136. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/batch_import_result.html +0 -0
  137. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/completed_approvals.html +0 -0
  138. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/form_categories.html +0 -0
  139. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/form_list.html +0 -0
  140. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/form_submit.html +0 -0
  141. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/my_submissions.html +0 -0
  142. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/reassign_task.html +0 -0
  143. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/resubmit_confirm.html +0 -0
  144. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/sso/login.html +0 -0
  145. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/sub_workflow_detail.html +0 -0
  146. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/submission_bulk_pdf.html +0 -0
  147. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/submission_detail.html +0 -0
  148. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/submission_pdf.html +0 -0
  149. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/withdraw_confirm.html +0 -0
  150. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/approval_notification.html +0 -0
  151. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/approval_reminder.html +0 -0
  152. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/approval_request.html +0 -0
  153. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/email_styles.html +0 -0
  154. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/escalation_notification.html +0 -0
  155. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/notification_digest.html +0 -0
  156. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/rejection_notification.html +0 -0
  157. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/submission_notification.html +0 -0
  158. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/registration/login.html +0 -0
  159. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templatetags/__init__.py +0 -0
  160. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templatetags/forms_workflows_tags.py +0 -0
  161. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/urls.py +0 -0
  162. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/workflow_builder_views.py +0 -0
  163. {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/workflow_engine.py +0 -0
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.37.0] - 2026-03-26
11
+
12
+ ### Added
13
+ - **`reviewer_groups` permission level on `FormDefinition`** — a new M2M field that grants named groups read-only access to all submissions and their full approval history for a form, without the management capabilities of `admin_groups`.
14
+ - Members of a `reviewer_groups` group will see all submitted/pending/approved/rejected/withdrawn submissions for that form under **My Submissions** (alongside their own), so they can navigate to them without needing a direct URL.
15
+ - The full approval history is always visible to reviewers even when `hide_approval_history` is enabled on the workflow (matching the behaviour for approvers and admins).
16
+ - Access is enforced consistently across `submission_detail`, `sub_workflow_detail`, `submission_pdf`, `bulk_export_submissions`, `bulk_export_submissions_pdf`, and the `user_can_view_submission` utility.
17
+ - The field is exposed in Django Admin under the **Access Control** fieldset with a horizontal filter widget, and is preserved when cloning a form.
18
+ - Migration `0064_add_reviewer_groups` adds the underlying join table.
19
+
20
+ ## [0.36.5] - 2026-03-26
21
+
22
+ ### Fixed
23
+ - **`email_to_field` / `email_cc_field` now support comma-separated addresses** — `EmailActionHandler._get_recipients` and `_get_cc_list` previously appended the raw field value as a single address, silently discarding all but the first recipient when multiple addresses were entered. Both methods now split on commas and validate each address individually, so a faculty-emails field with `prof1@sjcme.edu, prof2@sjcme.edu` correctly delivers to all listed recipients.
24
+
25
+ ## [0.36.4] - 2026-03-26
26
+
27
+ ### Added
28
+ - **Dynamic dropdown choices from database** — select, multiselect, multiselect_list, radio, and checkboxes fields can now have their options populated live from any SQL query defined in `FORMS_WORKFLOWS_DATABASE_QUERIES`. Mark a query with `"return_choices": True` (and no user parameter) to return multiple rows; each row's first column becomes the option value and the second column (if present) becomes the display label. Wire it up by pointing the field's PrefillSource at the query key — the stored `choices` JSON is used as a fallback if the DB is unavailable.
29
+ - `DatabaseDataSource.execute_choices_query(query_key)` — new method that executes a choices query without a user parameter and returns `[(value, label), ...]`.
30
+ - `_get_choices_from_prefill_source(field_def)` — new helper on both form classes that detects `return_choices` queries and calls `execute_choices_query`; returns `None` when not applicable so callers fall back to stored choices.
31
+ - `execute_custom_query` now warns and returns `None` if called on a `return_choices` query, preventing accidental single-value use.
32
+
33
+ ## [0.36.3] - 2026-03-26
34
+
35
+ ### Fixed
36
+ - **Cross-instance sync missing newer fields** — `sync_api.serialize_form` and `_serialize_field` were not including several fields added after the serializer was first written, meaning push/pull/diff silently dropped their values on the receiving instance:
37
+ - `FormDefinition.allow_batch_import` (spreadsheet/Excel batch upload toggle, migration 0062)
38
+ - `FormDefinition.is_listed` (hide-from-list-page flag, migration 0051)
39
+ - `FormDefinition.allow_resubmit` (re-submission from rejected/withdrawn, migration 0024)
40
+ - `FormDefinition.api_enabled` (REST API exposure flag, migration 0063)
41
+ - `FormField.formula` (formula expression for Calculated field types, migration 0056)
42
+ All five fields are now serialized on export and restored on import. The diff summary in `diff_views._build_summary` is also updated to detect changes to all four FormDefinition flags.
43
+
10
44
  ## [0.33.1] - 2026-03-23
11
45
 
12
46
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-forms-workflows
3
- Version: 0.36.2
3
+ Version: 0.37.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
@@ -398,7 +398,12 @@ class FormDefinitionAdmin(nested_admin.NestedModelAdmin):
398
398
  prepopulated_fields = {"slug": ("name",)}
399
399
  readonly_fields = ("created_at", "updated_at", "created_by")
400
400
  inlines = [FormFieldInline, WorkflowDefinitionInline]
401
- filter_horizontal = ("submit_groups", "view_groups", "admin_groups")
401
+ filter_horizontal = (
402
+ "submit_groups",
403
+ "view_groups",
404
+ "admin_groups",
405
+ "reviewer_groups",
406
+ )
402
407
  change_form_template = "admin/django_forms_workflows/formdef_change_form.html"
403
408
  actions = ["clone_forms", "diff_forms", "export_as_json"]
404
409
 
@@ -424,6 +429,7 @@ class FormDefinitionAdmin(nested_admin.NestedModelAdmin):
424
429
  "submit_groups",
425
430
  "view_groups",
426
431
  "admin_groups",
432
+ "reviewer_groups",
427
433
  ),
428
434
  },
429
435
  ),
@@ -614,6 +620,7 @@ class FormDefinitionAdmin(nested_admin.NestedModelAdmin):
614
620
  cloned_form.submit_groups.set(form.submit_groups.all())
615
621
  cloned_form.view_groups.set(form.view_groups.all())
616
622
  cloned_form.admin_groups.set(form.admin_groups.all())
623
+ cloned_form.reviewer_groups.set(form.reviewer_groups.all())
617
624
 
618
625
  # Clone workflows, stages, and stage approval groups
619
626
  for wf in form.workflows.all():
@@ -428,6 +428,16 @@ class DatabaseDataSource(DataSource):
428
428
  )
429
429
  return None
430
430
 
431
+ # Choices queries (return_choices=True) return multiple rows for
432
+ # populating dropdown options and require no user parameter.
433
+ # Use execute_choices_query() for those instead.
434
+ if query_config.get("return_choices"):
435
+ logger.warning(
436
+ f"Database query '{query_key}' has return_choices=True. "
437
+ "Use execute_choices_query() to get dropdown options."
438
+ )
439
+ return None
440
+
431
441
  # Get user's lookup value
432
442
  user_id = self._get_user_id(user, user_field)
433
443
  if not user_id:
@@ -461,6 +471,76 @@ class DatabaseDataSource(DataSource):
461
471
  logger.error(f"Error executing custom query '{query_key}': {e}")
462
472
  return None
463
473
 
474
+ def execute_choices_query(self, query_key: str) -> list[tuple] | None:
475
+ """
476
+ Execute a reference-data query and return all rows as (value, label) tuples
477
+ for populating dropdown / select / radio field options.
478
+
479
+ Unlike execute_custom_query(), this method requires no user parameter and
480
+ always fetches all rows. The query config must have ``return_choices: True``.
481
+
482
+ If the query returns two columns, the first is used as the option value and
483
+ the second as the display label. If only one column is returned, both value
484
+ and label are set to that column's value.
485
+
486
+ Args:
487
+ query_key: Key from FORMS_WORKFLOWS_DATABASE_QUERIES setting.
488
+
489
+ Returns:
490
+ List of (value, label) tuples, or None on configuration/query error.
491
+ """
492
+ try:
493
+ queries = getattr(settings, "FORMS_WORKFLOWS_DATABASE_QUERIES", {})
494
+ if query_key not in queries:
495
+ logger.error(
496
+ f"Database query key '{query_key}' not found in "
497
+ "FORMS_WORKFLOWS_DATABASE_QUERIES setting"
498
+ )
499
+ return None
500
+
501
+ query_config = queries[query_key]
502
+
503
+ if not query_config.get("return_choices"):
504
+ logger.error(
505
+ f"Database query '{query_key}' does not have return_choices=True. "
506
+ "Use execute_custom_query() for single-value prefill queries."
507
+ )
508
+ return None
509
+
510
+ query_text = query_config.get("query")
511
+ db_alias = query_config.get("db_alias")
512
+
513
+ if not query_text or not db_alias:
514
+ logger.error(
515
+ f"Database choices query '{query_key}' missing 'query' or 'db_alias'"
516
+ )
517
+ return None
518
+
519
+ databases = getattr(settings, "DATABASES", {})
520
+ if db_alias not in databases:
521
+ logger.error(f"Database alias '{db_alias}' not configured")
522
+ return None
523
+
524
+ with connections[db_alias].cursor() as cursor:
525
+ cursor.execute(query_text)
526
+ rows = cursor.fetchall()
527
+
528
+ results = []
529
+ for row in rows:
530
+ if len(row) >= 2:
531
+ value = str(row[0] or "").strip()
532
+ label = str(row[1] or "").strip()
533
+ else:
534
+ value = label = str(row[0] or "").strip()
535
+ if value:
536
+ results.append((value, label))
537
+
538
+ return results
539
+
540
+ except Exception as e:
541
+ logger.error(f"Error executing choices query '{query_key}': {e}")
542
+ return None
543
+
464
544
  def is_available(self) -> bool:
465
545
  """
466
546
  Check if database source is configured.
@@ -150,13 +150,17 @@ def _build_summary(forms_data):
150
150
  "description",
151
151
  "instructions",
152
152
  "is_active",
153
+ "is_listed",
153
154
  "allow_save_draft",
154
155
  "allow_withdrawal",
156
+ "allow_resubmit",
157
+ "allow_batch_import",
155
158
  "requires_login",
156
159
  "enable_multi_step",
157
160
  "enable_auto_save",
158
161
  "auto_save_interval",
159
162
  "pdf_generation",
163
+ "api_enabled",
160
164
  ]
161
165
  for key in meta_keys:
162
166
  if b_form.get(key) != o_form.get(key):
@@ -276,6 +276,31 @@ class DynamicForm(forms.Form):
276
276
 
277
277
  return []
278
278
 
279
+ def _get_choices_from_prefill_source(self, field_def):
280
+ """
281
+ Return a list of (value, label) tuples from a database choices query when the
282
+ field's PrefillSource points to a query with ``return_choices=True``.
283
+
284
+ Returns None if the field has no such source (so the caller can fall back to
285
+ the stored ``field_def.choices``). Returns [] if the query ran but was empty.
286
+ """
287
+ prefill_key = field_def.get_prefill_source_key()
288
+ if not prefill_key or not prefill_key.startswith("dbquery."):
289
+ return None
290
+
291
+ query_key = prefill_key[len("dbquery.") :]
292
+
293
+ from django.conf import settings
294
+
295
+ queries = getattr(settings, "FORMS_WORKFLOWS_DATABASE_QUERIES", {})
296
+ if not queries.get(query_key, {}).get("return_choices"):
297
+ return None
298
+
299
+ from .data_sources import DatabaseDataSource
300
+
301
+ source = DatabaseDataSource()
302
+ return source.execute_choices_query(query_key)
303
+
279
304
  def add_field(self, field_def, initial_data):
280
305
  """Add a field to the form based on field definition"""
281
306
 
@@ -402,19 +427,34 @@ class DynamicForm(forms.Form):
402
427
  self.fields[field_def.field_name] = forms.URLField(**field_args)
403
428
 
404
429
  elif field_def.field_type == "select":
405
- choices = [("", "-- Select --")] + self._parse_choices(field_def.choices)
430
+ _db_choices = self._get_choices_from_prefill_source(field_def)
431
+ choices = [("", "-- Select --")] + (
432
+ _db_choices
433
+ if _db_choices is not None
434
+ else self._parse_choices(field_def.choices)
435
+ )
406
436
  self.fields[field_def.field_name] = forms.ChoiceField(
407
437
  choices=choices, **field_args
408
438
  )
409
439
 
410
440
  elif field_def.field_type == "multiselect":
411
- choices = self._parse_choices(field_def.choices)
441
+ _db_choices = self._get_choices_from_prefill_source(field_def)
442
+ choices = (
443
+ _db_choices
444
+ if _db_choices is not None
445
+ else self._parse_choices(field_def.choices)
446
+ )
412
447
  self.fields[field_def.field_name] = forms.MultipleChoiceField(
413
448
  choices=choices, widget=forms.CheckboxSelectMultiple, **field_args
414
449
  )
415
450
 
416
451
  elif field_def.field_type == "multiselect_list":
417
- choices = self._parse_choices(field_def.choices)
452
+ _db_choices = self._get_choices_from_prefill_source(field_def)
453
+ choices = (
454
+ _db_choices
455
+ if _db_choices is not None
456
+ else self._parse_choices(field_def.choices)
457
+ )
418
458
  self.fields[field_def.field_name] = forms.MultipleChoiceField(
419
459
  choices=choices,
420
460
  widget=forms.SelectMultiple(attrs={"class": "form-select"}),
@@ -422,7 +462,12 @@ class DynamicForm(forms.Form):
422
462
  )
423
463
 
424
464
  elif field_def.field_type == "radio":
425
- choices = self._parse_choices(field_def.choices)
465
+ _db_choices = self._get_choices_from_prefill_source(field_def)
466
+ choices = (
467
+ _db_choices
468
+ if _db_choices is not None
469
+ else self._parse_choices(field_def.choices)
470
+ )
426
471
  self.fields[field_def.field_name] = forms.ChoiceField(
427
472
  choices=choices, widget=forms.RadioSelect, **field_args
428
473
  )
@@ -436,7 +481,12 @@ class DynamicForm(forms.Form):
436
481
  )
437
482
 
438
483
  elif field_def.field_type == "checkboxes":
439
- choices = self._parse_choices(field_def.choices)
484
+ _db_choices = self._get_choices_from_prefill_source(field_def)
485
+ choices = (
486
+ _db_choices
487
+ if _db_choices is not None
488
+ else self._parse_choices(field_def.choices)
489
+ )
440
490
  self.fields[field_def.field_name] = forms.MultipleChoiceField(
441
491
  choices=choices,
442
492
  widget=forms.CheckboxSelectMultiple,
@@ -1057,7 +1107,12 @@ class ApprovalStepForm(forms.Form):
1057
1107
  self.fields[field_def.field_name] = forms.EmailField(**field_args)
1058
1108
 
1059
1109
  elif field_def.field_type == "select":
1060
- choices = [("", "-- Select --")] + self._parse_choices(field_def.choices)
1110
+ _db_choices = self._get_choices_from_prefill_source(field_def)
1111
+ choices = [("", "-- Select --")] + (
1112
+ _db_choices
1113
+ if _db_choices is not None
1114
+ else self._parse_choices(field_def.choices)
1115
+ )
1061
1116
  if widget_attrs:
1062
1117
  field_args["widget"] = forms.Select(attrs=widget_attrs)
1063
1118
  self.fields[field_def.field_name] = forms.ChoiceField(
@@ -1065,7 +1120,12 @@ class ApprovalStepForm(forms.Form):
1065
1120
  )
1066
1121
 
1067
1122
  elif field_def.field_type == "multiselect":
1068
- choices = self._parse_choices(field_def.choices)
1123
+ _db_choices = self._get_choices_from_prefill_source(field_def)
1124
+ choices = (
1125
+ _db_choices
1126
+ if _db_choices is not None
1127
+ else self._parse_choices(field_def.choices)
1128
+ )
1069
1129
  self.fields[field_def.field_name] = forms.MultipleChoiceField(
1070
1130
  choices=choices,
1071
1131
  widget=forms.CheckboxSelectMultiple,
@@ -1073,7 +1133,12 @@ class ApprovalStepForm(forms.Form):
1073
1133
  )
1074
1134
 
1075
1135
  elif field_def.field_type == "multiselect_list":
1076
- choices = self._parse_choices(field_def.choices)
1136
+ _db_choices = self._get_choices_from_prefill_source(field_def)
1137
+ choices = (
1138
+ _db_choices
1139
+ if _db_choices is not None
1140
+ else self._parse_choices(field_def.choices)
1141
+ )
1077
1142
  self.fields[field_def.field_name] = forms.MultipleChoiceField(
1078
1143
  choices=choices,
1079
1144
  widget=forms.SelectMultiple(attrs={"class": "form-select"}),
@@ -1081,7 +1146,12 @@ class ApprovalStepForm(forms.Form):
1081
1146
  )
1082
1147
 
1083
1148
  elif field_def.field_type == "radio":
1084
- choices = self._parse_choices(field_def.choices)
1149
+ _db_choices = self._get_choices_from_prefill_source(field_def)
1150
+ choices = (
1151
+ _db_choices
1152
+ if _db_choices is not None
1153
+ else self._parse_choices(field_def.choices)
1154
+ )
1085
1155
  self.fields[field_def.field_name] = forms.ChoiceField(
1086
1156
  choices=choices,
1087
1157
  widget=forms.RadioSelect(attrs=widget_attrs),
@@ -1195,6 +1265,29 @@ class ApprovalStepForm(forms.Form):
1195
1265
  return [(c.strip(), c.strip()) for c in choices.split(",") if c.strip()]
1196
1266
  return []
1197
1267
 
1268
+ def _get_choices_from_prefill_source(self, field_def):
1269
+ """
1270
+ Return (value, label) tuples from a database choices query when the field's
1271
+ PrefillSource points to a query with ``return_choices=True``, or None if the
1272
+ field has no such source (so the caller falls back to stored choices).
1273
+ """
1274
+ prefill_key = field_def.get_prefill_source_key()
1275
+ if not prefill_key or not prefill_key.startswith("dbquery."):
1276
+ return None
1277
+
1278
+ query_key = prefill_key[len("dbquery.") :]
1279
+
1280
+ from django.conf import settings
1281
+
1282
+ queries = getattr(settings, "FORMS_WORKFLOWS_DATABASE_QUERIES", {})
1283
+ if not queries.get(query_key, {}).get("return_choices"):
1284
+ return None
1285
+
1286
+ from .data_sources import DatabaseDataSource
1287
+
1288
+ source = DatabaseDataSource()
1289
+ return source.execute_choices_query(query_key)
1290
+
1198
1291
  def _build_layout_fields(self, field_defs):
1199
1292
  """Convert a list of field definitions into crispy layout elements,
1200
1293
  respecting half / third / fourth width settings."""
@@ -95,11 +95,14 @@ class EmailHandler(BaseActionHandler):
95
95
  ]
96
96
  recipients.extend(static_emails)
97
97
 
98
- # Add email from form field
98
+ # Add email(s) from form field — supports comma-separated addresses
99
99
  if self.action.email_to_field:
100
100
  field_email = self.get_form_field_value(self.action.email_to_field)
101
- if field_email and isinstance(field_email, str) and "@" in field_email:
102
- recipients.append(field_email.strip())
101
+ if field_email and isinstance(field_email, str):
102
+ for addr in field_email.split(","):
103
+ addr = addr.strip()
104
+ if addr and "@" in addr:
105
+ recipients.append(addr)
103
106
 
104
107
  # Remove duplicates while preserving order
105
108
  seen = set()
@@ -127,11 +130,14 @@ class EmailHandler(BaseActionHandler):
127
130
  ]
128
131
  cc_list.extend(static_cc)
129
132
 
130
- # Add CC from form field
133
+ # Add CC email(s) from form field — supports comma-separated addresses
131
134
  if self.action.email_cc_field:
132
135
  field_cc = self.get_form_field_value(self.action.email_cc_field)
133
- if field_cc and isinstance(field_cc, str) and "@" in field_cc:
134
- cc_list.append(field_cc.strip())
136
+ if field_cc and isinstance(field_cc, str):
137
+ for addr in field_cc.split(","):
138
+ addr = addr.strip()
139
+ if addr and "@" in addr:
140
+ cc_list.append(addr)
135
141
 
136
142
  # Remove duplicates
137
143
  seen = set()
@@ -0,0 +1,27 @@
1
+ import django.db.models.deletion
2
+ from django.db import migrations, models
3
+
4
+
5
+ class Migration(migrations.Migration):
6
+
7
+ dependencies = [
8
+ ("django_forms_workflows", "0063_formdefinition_api_enabled_apitoken"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="formdefinition",
14
+ name="reviewer_groups",
15
+ field=models.ManyToManyField(
16
+ blank=True,
17
+ help_text=(
18
+ "Groups that can view all submissions and full approval history "
19
+ "for this form. Unlike admin groups, reviewers cannot manage the "
20
+ "form itself."
21
+ ),
22
+ related_name="can_review_forms",
23
+ to="auth.group",
24
+ ),
25
+ ),
26
+ ]
27
+
@@ -156,6 +156,15 @@ class FormDefinition(models.Model):
156
156
  blank=True,
157
157
  help_text="Groups that can view all submissions",
158
158
  )
159
+ reviewer_groups = models.ManyToManyField(
160
+ Group,
161
+ related_name="can_review_forms",
162
+ blank=True,
163
+ help_text=(
164
+ "Groups that can view all submissions and full approval history for this form. "
165
+ "Unlike admin groups, reviewers cannot manage the form itself."
166
+ ),
167
+ )
159
168
 
160
169
  # Behavior
161
170
  allow_save_draft = models.BooleanField(
@@ -216,6 +216,7 @@ def _serialize_field(field):
216
216
  "choices": field.choices,
217
217
  "prefill_source_config": _serialize_prefill_source(field.prefill_source_config),
218
218
  "default_value": field.default_value,
219
+ "formula": field.formula,
219
220
  "conditional_rules": field.conditional_rules,
220
221
  "validation_rules": field.validation_rules,
221
222
  "field_dependencies": field.field_dependencies,
@@ -373,18 +374,22 @@ def serialize_form(form_definition):
373
374
  "description": form_definition.description,
374
375
  "instructions": form_definition.instructions,
375
376
  "is_active": form_definition.is_active,
377
+ "is_listed": form_definition.is_listed,
376
378
  "version": form_definition.version,
377
379
  "submit_groups": _group_names(form_definition.submit_groups),
378
380
  "view_groups": _group_names(form_definition.view_groups),
379
381
  "admin_groups": _group_names(form_definition.admin_groups),
380
382
  "allow_save_draft": form_definition.allow_save_draft,
381
383
  "allow_withdrawal": form_definition.allow_withdrawal,
384
+ "allow_resubmit": form_definition.allow_resubmit,
385
+ "allow_batch_import": form_definition.allow_batch_import,
382
386
  "requires_login": form_definition.requires_login,
383
387
  "enable_multi_step": form_definition.enable_multi_step,
384
388
  "form_steps": form_definition.form_steps,
385
389
  "enable_auto_save": form_definition.enable_auto_save,
386
390
  "auto_save_interval": form_definition.auto_save_interval,
387
391
  "pdf_generation": form_definition.pdf_generation,
392
+ "api_enabled": form_definition.api_enabled,
388
393
  },
389
394
  "fields": [
390
395
  _serialize_field(f) for f in form_definition.fields.all().order_by("order")
@@ -521,16 +526,20 @@ def import_form(form_data, conflict="update", category_cache=None):
521
526
  "description": fd.get("description", ""),
522
527
  "instructions": fd.get("instructions", ""),
523
528
  "is_active": fd.get("is_active", True),
529
+ "is_listed": fd.get("is_listed", True),
524
530
  "version": fd.get("version", 1),
525
531
  "category": category,
526
532
  "allow_save_draft": fd.get("allow_save_draft", False),
527
533
  "allow_withdrawal": fd.get("allow_withdrawal", False),
534
+ "allow_resubmit": fd.get("allow_resubmit", False),
535
+ "allow_batch_import": fd.get("allow_batch_import", False),
528
536
  "requires_login": fd.get("requires_login", True),
529
537
  "enable_multi_step": fd.get("enable_multi_step", False),
530
538
  "form_steps": fd.get("form_steps"),
531
539
  "enable_auto_save": fd.get("enable_auto_save", False),
532
540
  "auto_save_interval": fd.get("auto_save_interval", 30),
533
541
  "pdf_generation": fd.get("pdf_generation", False),
542
+ "api_enabled": fd.get("api_enabled", False),
534
543
  }
535
544
 
536
545
  form_obj, created = FormDefinition.objects.update_or_create(
@@ -264,6 +264,12 @@ def user_can_view_submission(user, submission: FormSubmission) -> bool:
264
264
  ).exists():
265
265
  return True
266
266
 
267
+ # Reviewers can view submissions and approval history
268
+ if user.groups.filter(
269
+ id__in=submission.form_definition.reviewer_groups.all()
270
+ ).exists():
271
+ return True
272
+
267
273
  return False
268
274
 
269
275
 
@@ -417,7 +417,25 @@ def my_submissions(request):
417
417
  ``category_counts`` list so the UI can render a filter-pill bar showing
418
418
  how many submissions belong to each category.
419
419
  """
420
- base_submissions = FormSubmission.objects.filter(submitter=request.user)
420
+ # Include submissions the user owns OR submissions for forms they can review.
421
+ reviewer_form_ids = (
422
+ FormDefinition.objects.filter(reviewer_groups__in=request.user.groups.all())
423
+ .values_list("id", flat=True)
424
+ .distinct()
425
+ )
426
+ base_submissions = FormSubmission.objects.filter(
427
+ models.Q(submitter=request.user)
428
+ | models.Q(
429
+ form_definition__in=reviewer_form_ids,
430
+ status__in=[
431
+ "submitted",
432
+ "pending_approval",
433
+ "approved",
434
+ "rejected",
435
+ "withdrawn",
436
+ ],
437
+ )
438
+ ).distinct()
421
439
 
422
440
  # --- Category counts (for the filter bar) ---
423
441
  raw_counts = (
@@ -542,14 +560,17 @@ def submission_detail(request, submission_id):
542
560
  """View submission details"""
543
561
  submission = get_object_or_404(FormSubmission, id=submission_id)
544
562
 
545
- # Check permissions - user must be submitter, approver, or admin
563
+ # Check permissions - user must be submitter, approver, admin, or reviewer
564
+ form_def_early = submission.form_definition
565
+ is_reviewer = request.user.groups.filter(
566
+ id__in=form_def_early.reviewer_groups.all()
567
+ ).exists()
546
568
  can_view = (
547
569
  submission.submitter == request.user
548
570
  or request.user.is_superuser
549
571
  or user_can_approve(request.user, submission)
550
- or request.user.groups.filter(
551
- id__in=submission.form_definition.admin_groups.all()
552
- ).exists()
572
+ or request.user.groups.filter(id__in=form_def_early.admin_groups.all()).exists()
573
+ or is_reviewer
553
574
  )
554
575
 
555
576
  if not can_view:
@@ -743,15 +764,17 @@ def submission_detail(request, submission_id):
743
764
  request.user.is_superuser
744
765
  or user_can_approve(request.user, submission)
745
766
  or request.user.groups.filter(id__in=form_def.admin_groups.all()).exists()
767
+ or is_reviewer
746
768
  )
747
769
 
748
770
  # Privacy: hide approval history from the submitter when configured.
749
- # Approvers and admins always see the full history.
771
+ # Approvers, admins, and reviewers always see the full history.
750
772
  is_submitter_only = (
751
773
  submission.submitter == request.user
752
774
  and not request.user.is_superuser
753
775
  and not user_can_approve(request.user, submission)
754
776
  and not request.user.groups.filter(id__in=form_def.admin_groups.all()).exists()
777
+ and not is_reviewer
755
778
  )
756
779
  hide_approval_history = bool(
757
780
  workflow and workflow.hide_approval_history and is_submitter_only
@@ -1424,6 +1447,9 @@ def sub_workflow_detail(request, instance_id):
1424
1447
  or request.user.groups.filter(
1425
1448
  id__in=submission.form_definition.admin_groups.all()
1426
1449
  ).exists()
1450
+ or request.user.groups.filter(
1451
+ id__in=submission.form_definition.reviewer_groups.all()
1452
+ ).exists()
1427
1453
  )
1428
1454
  if not can_view:
1429
1455
  messages.error(request, "You don't have permission to view this.")
@@ -1789,6 +1815,7 @@ def submission_pdf(request, submission_id):
1789
1815
  or request.user.is_superuser
1790
1816
  or user_can_approve(request.user, submission)
1791
1817
  or request.user.groups.filter(id__in=form_def.admin_groups.all()).exists()
1818
+ or request.user.groups.filter(id__in=form_def.reviewer_groups.all()).exists()
1792
1819
  )
1793
1820
  if not can_view:
1794
1821
  return HttpResponseForbidden(
@@ -2555,6 +2582,9 @@ def bulk_export_submissions(request):
2555
2582
  or request.user.groups.filter(
2556
2583
  id__in=sub.form_definition.admin_groups.all()
2557
2584
  ).exists()
2585
+ or request.user.groups.filter(
2586
+ id__in=sub.form_definition.reviewer_groups.all()
2587
+ ).exists()
2558
2588
  )
2559
2589
  if can_view:
2560
2590
  allowed.append(sub)
@@ -2738,6 +2768,9 @@ def bulk_export_submissions_pdf(request):
2738
2768
  or request.user.groups.filter(
2739
2769
  id__in=sub.form_definition.admin_groups.all()
2740
2770
  ).exists()
2771
+ or request.user.groups.filter(
2772
+ id__in=sub.form_definition.reviewer_groups.all()
2773
+ ).exists()
2741
2774
  )
2742
2775
  if can_view:
2743
2776
  allowed.append(sub)
@@ -3145,7 +3178,24 @@ def my_submissions_ajax(request):
3145
3178
  category_slug = params.get("category", "").strip()
3146
3179
  form_slug = params.get("form", "").strip()
3147
3180
 
3148
- qs = FormSubmission.objects.filter(submitter=request.user)
3181
+ reviewer_form_ids = (
3182
+ FormDefinition.objects.filter(reviewer_groups__in=request.user.groups.all())
3183
+ .values_list("id", flat=True)
3184
+ .distinct()
3185
+ )
3186
+ qs = FormSubmission.objects.filter(
3187
+ models.Q(submitter=request.user)
3188
+ | models.Q(
3189
+ form_definition__in=reviewer_form_ids,
3190
+ status__in=[
3191
+ "submitted",
3192
+ "pending_approval",
3193
+ "approved",
3194
+ "rejected",
3195
+ "withdrawn",
3196
+ ],
3197
+ )
3198
+ ).distinct()
3149
3199
  records_total = qs.count()
3150
3200
 
3151
3201
  if category_slug:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-forms-workflows"
3
- version = "0.36.2"
3
+ version = "0.37.0"
4
4
  description = "Enterprise-grade, database-driven form builder with approval workflows and external data integration"
5
5
  license = "LGPL-3.0-only"
6
6
  readme = "README.md"