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.
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/CHANGELOG.md +34 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/PKG-INFO +1 -1
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/admin.py +8 -1
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/database_source.py +80 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/diff_views.py +4 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/forms.py +102 -9
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/email_handler.py +12 -6
- django_forms_workflows-0.37.0/django_forms_workflows/migrations/0064_add_reviewer_groups.py +27 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/models.py +9 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sync_api.py +9 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/utils.py +6 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/views.py +57 -7
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/pyproject.toml +1 -1
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/LICENSE +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/README.md +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/__init__.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/api_urls.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/api_views.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/apps.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/conditions.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/context_processors.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/__init__.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/base.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/ldap_source.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/data_sources/user_source.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/email_backends/__init__.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/email_backends/gmail_api.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/form_builder_urls.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/form_builder_views.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/__init__.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/api_handler.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/base.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/database_handler.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/executor.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/file_handler.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/handlers/ldap_handler.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/ldap_backend.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/create_default_templates.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/pull_forms.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/push_forms.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/seed_farm_demo.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/seed_pcn_form.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/seed_prefill_sources.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/sync_ldap_profiles.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/sync_ldap_users.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/management/commands/test_db_connection.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0001_initial.py +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0003_postsubmissionaction.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0004_formtemplate.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0005_advanced_conditional_logic.py +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0007_add_userprofile_ldap_enhancements.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0008_add_file_workflow_models.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0009_add_formfield_readonly.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0010_add_prefillsource_template_fields.py +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0012_add_approval_step_fields.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0013_add_database_query_key.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0014_add_form_category.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0015_add_workflowstage_staged_approvals.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0016_add_pdf_generation.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0017_add_notification_batching.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0018_add_form_category_parent.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0019_add_ldap_group_profile.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0020_add_bulk_export_field.py +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0022_add_bulk_pdf_export.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0023_add_performance_indexes.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0024_add_allow_resubmit.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0025_workflowstage_approve_label.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0026_alter_formfield_width.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0027_formsubmission_form_data_gin.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0028_workflowstagegroupconfig.py +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0030_formfield_phone_type.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0031_sub_workflows.py +0 -0
- {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
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0034_add_notificationlog.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0035_migrate_prefill_source_strings.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0036_add_workflowstage_auto_created.py +0 -0
- {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
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0039_update_help_text.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0040_migrate_deprecated_features.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0041_remove_deprecated_fields.py +0 -0
- {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
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0044_remove_subworkflowinstance_label.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0045_stageapprovalgroup_workflow_name_label.py +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0047_add_hide_approval_history.py +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0049_formfield_multifile_type.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0050_workflowdefinition_collapse_parallel_stages.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0051_formdefinition_is_listed.py +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0053_stageformfieldnotification_static_emails.py +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0055_add_send_back.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0056_calculated_formula_spreadsheet.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0057_add_currency_field_type.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/0063_formdefinition_api_enabled_apitoken.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/migrations/__init__.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/signals.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sso_backends.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sso_urls.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sso_views.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sync_views.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/tasks.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/_category_node.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/api/docs.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/approval_inbox.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/approve.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/base.html +0 -0
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/completed_approvals.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/form_categories.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/form_list.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/form_submit.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/my_submissions.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/reassign_task.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/resubmit_confirm.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/sso/login.html +0 -0
- {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
- {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
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/submission_detail.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/submission_pdf.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/django_forms_workflows/withdraw_confirm.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/approval_notification.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/approval_reminder.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/approval_request.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/email_styles.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/escalation_notification.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/notification_digest.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/rejection_notification.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/emails/submission_notification.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templates/registration/login.html +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templatetags/__init__.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/templatetags/forms_workflows_tags.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/urls.py +0 -0
- {django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/workflow_builder_views.py +0 -0
- {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
|
{django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/admin.py
RENAMED
|
@@ -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 = (
|
|
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.
|
{django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/diff_views.py
RENAMED
|
@@ -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):
|
{django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/forms.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
102
|
-
|
|
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)
|
|
134
|
-
|
|
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
|
+
|
{django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/models.py
RENAMED
|
@@ -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(
|
{django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/sync_api.py
RENAMED
|
@@ -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(
|
{django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/utils.py
RENAMED
|
@@ -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
|
|
{django_forms_workflows-0.36.2 → django_forms_workflows-0.37.0}/django_forms_workflows/views.py
RENAMED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
File without changes
|
|
File without changes
|