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