velocity-python 0.0.234__tar.gz → 0.0.236__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. {velocity_python-0.0.234/src/velocity_python.egg-info → velocity_python-0.0.236}/PKG-INFO +1 -1
  2. {velocity_python-0.0.234 → velocity_python-0.0.236}/pyproject.toml +1 -1
  3. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.0.236/src/velocity/app/validators/__init__.py +1 -0
  5. velocity_python-0.0.236/src/velocity/app/validators/formbuilder_template.py +204 -0
  6. {velocity_python-0.0.234 → velocity_python-0.0.236/src/velocity_python.egg-info}/PKG-INFO +1 -1
  7. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity_python.egg-info/SOURCES.txt +3 -0
  8. velocity_python-0.0.236/tests/test_formbuilder_template_validator.py +240 -0
  9. {velocity_python-0.0.234 → velocity_python-0.0.236}/LICENSE +0 -0
  10. {velocity_python-0.0.234 → velocity_python-0.0.236}/README.md +0 -0
  11. {velocity_python-0.0.234 → velocity_python-0.0.236}/setup.cfg +0 -0
  12. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/app/__init__.py +0 -0
  13. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/app/invoices.py +0 -0
  14. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/app/orders.py +0 -0
  15. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/app/payments.py +0 -0
  16. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/app/purchase_orders.py +0 -0
  17. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/app/tests/__init__.py +0 -0
  18. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/app/tests/test_email_processing.py +0 -0
  19. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/app/tests/test_payment_profile_sorting.py +0 -0
  20. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/app/tests/test_spreadsheet_functions.py +0 -0
  21. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/__init__.py +0 -0
  22. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/amplify.py +0 -0
  23. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/amplify_build.py +0 -0
  24. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/__init__.py +0 -0
  25. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/base_handler.py +0 -0
  26. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/context.py +0 -0
  27. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/context_factory.py +0 -0
  28. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/exceptions.py +0 -0
  29. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  30. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/mixins/__init__.py +0 -0
  31. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/mixins/data_service.py +0 -0
  32. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/mixins/web_handler.py +0 -0
  33. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/perf.py +0 -0
  34. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/response.py +0 -0
  35. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  36. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/tests/__init__.py +0 -0
  37. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/tests/test_base_handler_error_response.py +0 -0
  38. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/tests/test_lambda_handler_json_serialization.py +0 -0
  39. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/aws/tests/test_response.py +0 -0
  40. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/__init__.py +0 -0
  41. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/__init__.py +0 -0
  42. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/column.py +0 -0
  43. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/database.py +0 -0
  44. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/decorators.py +0 -0
  45. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/engine.py +0 -0
  46. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/result.py +0 -0
  47. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/row.py +0 -0
  48. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/sequence.py +0 -0
  49. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/table.py +0 -0
  50. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/transaction.py +0 -0
  51. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/core/view.py +0 -0
  52. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/exceptions.py +0 -0
  53. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/__init__.py +0 -0
  54. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/base/__init__.py +0 -0
  55. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/base/initializer.py +0 -0
  56. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/base/operators.py +0 -0
  57. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/base/sql.py +0 -0
  58. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/base/types.py +0 -0
  59. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/__init__.py +0 -0
  60. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/operators.py +0 -0
  61. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/reserved.py +0 -0
  62. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/sql.py +0 -0
  63. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/mysql/types.py +0 -0
  64. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/__init__.py +0 -0
  65. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/operators.py +0 -0
  66. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/reserved.py +0 -0
  67. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/sql.py +0 -0
  68. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/postgres/types.py +0 -0
  69. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/__init__.py +0 -0
  70. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/operators.py +0 -0
  71. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/reserved.py +0 -0
  72. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/sql.py +0 -0
  73. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlite/types.py +0 -0
  74. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/__init__.py +0 -0
  75. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/operators.py +0 -0
  76. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/reserved.py +0 -0
  77. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/sql.py +0 -0
  78. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/sqlserver/types.py +0 -0
  79. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/servers/tablehelper.py +0 -0
  80. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/__init__.py +0 -0
  81. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/common_db_test.py +0 -0
  82. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/__init__.py +0 -0
  83. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/common.py +0 -0
  84. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_column.py +0 -0
  85. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_connections.py +0 -0
  86. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_database.py +0 -0
  87. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_engine.py +0 -0
  88. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_general_usage.py +0 -0
  89. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_imports.py +0 -0
  90. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_result.py +0 -0
  91. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_row.py +0 -0
  92. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_row_comprehensive.py +0 -0
  93. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_schema_locking.py +0 -0
  94. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_schema_locking_unit.py +0 -0
  95. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_sequence.py +0 -0
  96. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_sql_comprehensive.py +0 -0
  97. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_table.py +0 -0
  98. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_table_comprehensive.py +0 -0
  99. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/postgres/test_transaction.py +0 -0
  100. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/sql/__init__.py +0 -0
  101. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/sql/common.py +0 -0
  102. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/sql/test_postgres_select_advanced.py +0 -0
  103. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/sql/test_postgres_select_variances.py +0 -0
  104. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_cursor_rowcount_fix.py +0 -0
  105. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_db_utils.py +0 -0
  106. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_postgres.py +0 -0
  107. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_postgres_unchanged.py +0 -0
  108. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_process_error_robustness.py +0 -0
  109. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_result_caching.py +0 -0
  110. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_result_sql_aware.py +0 -0
  111. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_row_get_missing_column.py +0 -0
  112. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_schema_locking_initializers.py +0 -0
  113. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_schema_locking_simple.py +0 -0
  114. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_sql_builder.py +0 -0
  115. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_tablehelper.py +0 -0
  116. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/tests/test_view_helper.py +0 -0
  117. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/db/utils.py +0 -0
  118. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/logging.py +0 -0
  119. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/__init__.py +0 -0
  120. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/conv/__init__.py +0 -0
  121. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/conv/iconv.py +0 -0
  122. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/conv/oconv.py +0 -0
  123. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/db.py +0 -0
  124. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/export.py +0 -0
  125. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/format.py +0 -0
  126. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/mail.py +0 -0
  127. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/merge.py +0 -0
  128. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tests/__init__.py +0 -0
  129. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tests/test_db.py +0 -0
  130. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tests/test_fix.py +0 -0
  131. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tests/test_format.py +0 -0
  132. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tests/test_iconv.py +0 -0
  133. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tests/test_merge.py +0 -0
  134. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tests/test_oconv.py +0 -0
  135. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tests/test_original_error.py +0 -0
  136. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tests/test_timer.py +0 -0
  137. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/timer.py +0 -0
  138. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/misc/tools.py +0 -0
  139. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/payment/__init__.py +0 -0
  140. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/payment/authorizenet_adapter.py +0 -0
  141. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/payment/base_adapter.py +0 -0
  142. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/payment/braintree_adapter.py +0 -0
  143. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/payment/charge_rules.py +0 -0
  144. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/payment/demo_profiles.py +0 -0
  145. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/payment/profiles.py +0 -0
  146. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/payment/router.py +0 -0
  147. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity/payment/stripe_adapter.py +0 -0
  148. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  149. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity_python.egg-info/requires.txt +0 -0
  150. {velocity_python-0.0.234 → velocity_python-0.0.236}/src/velocity_python.egg-info/top_level.txt +0 -0
  151. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_amplify_build.py +0 -0
  152. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_decorators.py +0 -0
  153. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_iconv_money_to_cents.py +0 -0
  154. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_lambda_handler.py +0 -0
  155. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_lambda_handler_auth.py +0 -0
  156. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_mixins_import.py +0 -0
  157. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_payment_braintree_adapter.py +0 -0
  158. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_payment_demo_profiles.py +0 -0
  159. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_payment_profiles.py +0 -0
  160. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_payment_router.py +0 -0
  161. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_payment_stripe_adapter.py +0 -0
  162. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_sys_modified_count_postgres_demo.py +0 -0
  163. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_table_alter.py +0 -0
  164. {velocity_python-0.0.234 → velocity_python-0.0.236}/tests/test_where_clause_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.234
3
+ Version: 0.0.236
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "velocity-python"
7
- version = "0.0.234"
7
+ version = "0.0.236"
8
8
  authors = [
9
9
  { name="Velocity Team", email="info@codeclubs.org" },
10
10
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.234"
1
+ __version__ = version = "0.0.236"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -0,0 +1 @@
1
+ """velocity.app.validators — domain validators for shared application entities."""
@@ -0,0 +1,204 @@
1
+ """Pure-Python validator for form_builder_template dicts.
2
+
3
+ Authoritative location: velocity.app.validators.formbuilder_template
4
+
5
+ Called by:
6
+ - BackOffice before_write hook (rwx/form_builder_template.py)
7
+ - BackOffice CLI: scripts/validate_formbuilder_templates.py
8
+ - DonateMain CLI: scripts/formbuilder_validator.py (shim — imports from here)
9
+ - Tests: velocity-python/tests/app/validators/test_formbuilder_template.py
10
+ BackOffice.demo/tests/backend/test_form_builder_template_hook.py
11
+ DonateMain.demo/tests/backend/test_formbuilder_validators.py
12
+
13
+ No external dependencies — only the stdlib so it runs everywhere the test suite runs.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from typing import Any, Dict, List
20
+
21
+ # --------------------------------------------------------------------------- #
22
+ # Helpers
23
+ # --------------------------------------------------------------------------- #
24
+
25
+ _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
26
+ # Also accept RFC 5322 display-name format: "Label <user@example.com>"
27
+ _EMAIL_DISPLAY_RE = re.compile(r"<[^@\s>]+@[^@\s>]+\.[^@\s>]+>$")
28
+ _URL_RE = re.compile(r"^https?://", re.IGNORECASE)
29
+
30
+ # Braintree: descriptor must be <= 22 characters, NAME*TEXT format.
31
+ _BT_DESCRIPTOR_MAX_LEN = 22
32
+
33
+
34
+ def _str_val(fb: Dict[str, Any], key: str) -> str:
35
+ """Return the stripped string value for a key, or '' when None/missing."""
36
+ v = fb.get(key)
37
+ return v.strip() if isinstance(v, str) else ""
38
+
39
+
40
+ # --------------------------------------------------------------------------- #
41
+ # Public API
42
+ # --------------------------------------------------------------------------- #
43
+
44
+ Issue = Dict[str, str] # {field, severity, message}
45
+
46
+
47
+ def validate_formbuilder_template(fb: Dict[str, Any]) -> List[Issue]:
48
+ """Validate a form_builder_template dict for known configuration problems.
49
+
50
+ Returns a list of issue dicts::
51
+
52
+ [{"field": "...", "severity": "error"|"warning", "message": "..."}]
53
+
54
+ An empty list means no issues were found.
55
+
56
+ Severities:
57
+ error – will cause a runtime crash or silent payment failure.
58
+ warning – likely produces a broken/confusing user experience.
59
+ """
60
+ issues: List[Issue] = []
61
+
62
+ def error(field: str, message: str) -> None:
63
+ issues.append({"field": field, "severity": "error", "message": message})
64
+
65
+ def warning(field: str, message: str) -> None:
66
+ issues.append({"field": field, "severity": "warning", "message": message})
67
+
68
+ # ---------------------------------------------------------------------- #
69
+ # Identity
70
+ # ---------------------------------------------------------------------- #
71
+ if not _str_val(fb, "client"):
72
+ error("client", "client is required; KeyError will crash the handler")
73
+
74
+ if not _str_val(fb, "campaign"):
75
+ error("campaign", "campaign is required; KeyError will crash the handler")
76
+
77
+ if not _str_val(fb, "url_slug"):
78
+ error("url_slug", "url_slug is required; template will not be routable")
79
+
80
+ # ---------------------------------------------------------------------- #
81
+ # Receipt email fields
82
+ # (used with direct key access: fb["receipt_from_address"] etc.)
83
+ # ---------------------------------------------------------------------- #
84
+ from_addr = _str_val(fb, "receipt_from_address")
85
+ if not from_addr:
86
+ error(
87
+ "receipt_from_address",
88
+ "receipt_from_address is empty; donation receipt emails will fail to send",
89
+ )
90
+ elif not _EMAIL_RE.match(from_addr) and not _EMAIL_DISPLAY_RE.search(from_addr):
91
+ error(
92
+ "receipt_from_address",
93
+ f"receipt_from_address '{from_addr}' is not a valid email address",
94
+ )
95
+
96
+ if not _str_val(fb, "receipt_subject"):
97
+ warning(
98
+ "receipt_subject",
99
+ "receipt_subject is empty; donation receipt email subject will be blank",
100
+ )
101
+
102
+ if not _str_val(fb, "receipt_body_text"):
103
+ error(
104
+ "receipt_body_text",
105
+ "receipt_body_text is empty; donation receipt emails will have no content",
106
+ )
107
+
108
+ # ---------------------------------------------------------------------- #
109
+ # Braintree transaction descriptor
110
+ # Rules: <= 22 chars, should contain '*', ASCII printable only.
111
+ # ---------------------------------------------------------------------- #
112
+ descriptor = _str_val(fb, "descriptor")
113
+ if descriptor:
114
+ if len(descriptor) > _BT_DESCRIPTOR_MAX_LEN:
115
+ error(
116
+ "descriptor",
117
+ f"descriptor is {len(descriptor)} chars (max {_BT_DESCRIPTOR_MAX_LEN}); "
118
+ "Braintree will reject the transaction",
119
+ )
120
+ if "*" not in descriptor:
121
+ warning(
122
+ "descriptor",
123
+ f"descriptor '{descriptor}' should follow 'NAME*TEXT' format for Braintree",
124
+ )
125
+ bad_chars = [c for c in descriptor if not (32 <= ord(c) <= 126)]
126
+ if bad_chars:
127
+ error(
128
+ "descriptor",
129
+ f"descriptor contains non-ASCII/non-printable characters: {bad_chars!r}",
130
+ )
131
+
132
+ # ---------------------------------------------------------------------- #
133
+ # Content — at least one active donation or metric must be configured
134
+ # ---------------------------------------------------------------------- #
135
+ has_content = _has_donation_content(fb) or _has_metric_content(fb)
136
+ if not has_content:
137
+ warning(
138
+ "donations/metrics",
139
+ "template has no donation amounts or metrics configured; the form will be empty",
140
+ )
141
+
142
+ # ---------------------------------------------------------------------- #
143
+ # Share URL (used in confirmation email)
144
+ # ---------------------------------------------------------------------- #
145
+ share_url = _str_val(fb, "share_url")
146
+ if share_url and not _URL_RE.match(share_url):
147
+ warning(
148
+ "share_url",
149
+ f"share_url '{share_url}' does not look like a valid URL (expected https://...)",
150
+ )
151
+
152
+ # ---------------------------------------------------------------------- #
153
+ # Confirmation page text
154
+ # ---------------------------------------------------------------------- #
155
+ if not _str_val(fb, "confirmation_header_text"):
156
+ warning(
157
+ "confirmation_header_text",
158
+ "confirmation_header_text is empty; the thank-you page header will be blank",
159
+ )
160
+
161
+ return issues
162
+
163
+
164
+ # --------------------------------------------------------------------------- #
165
+ # Internal helpers
166
+ # --------------------------------------------------------------------------- #
167
+
168
+
169
+ def _has_donation_content(fb: Dict[str, Any]) -> bool:
170
+ """Return True if at least one visible donation is configured."""
171
+ donations = fb.get("donations")
172
+ if isinstance(donations, dict):
173
+ for key, item in donations.items():
174
+ hide_key = f"donation_{key}_hide_donation_from_ui"
175
+ if fb.get(hide_key):
176
+ continue
177
+ if isinstance(item, dict) and item.get("label"):
178
+ return True
179
+ # Legacy numbered fields
180
+ for i in range(1, 10):
181
+ if fb.get(f"donation_{i}_hide_donation_from_ui"):
182
+ continue
183
+ if _str_val(fb, f"donation_{i}_label") or fb.get(f"donation_{i}_values"):
184
+ return True
185
+ return False
186
+
187
+
188
+ def _has_metric_content(fb: Dict[str, Any]) -> bool:
189
+ """Return True if at least one visible metric is configured."""
190
+ metrics = fb.get("metrics")
191
+ if isinstance(metrics, dict):
192
+ for key, item in metrics.items():
193
+ hide_key = f"{key}_hide_donation_from_ui"
194
+ if fb.get(hide_key):
195
+ continue
196
+ if isinstance(item, dict) and item.get("label"):
197
+ return True
198
+ # Legacy numbered fields
199
+ for i in range(1, 10):
200
+ if fb.get(f"metric_{i}_hide_donation_from_ui"):
201
+ continue
202
+ if _str_val(fb, f"metric_{i}_label") or fb.get(f"metric_{i}_values"):
203
+ return True
204
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.234
3
+ Version: 0.0.236
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Velocity Team <info@codeclubs.org>
6
6
  License-Expression: MIT
@@ -12,6 +12,8 @@ src/velocity/app/tests/__init__.py
12
12
  src/velocity/app/tests/test_email_processing.py
13
13
  src/velocity/app/tests/test_payment_profile_sorting.py
14
14
  src/velocity/app/tests/test_spreadsheet_functions.py
15
+ src/velocity/app/validators/__init__.py
16
+ src/velocity/app/validators/formbuilder_template.py
15
17
  src/velocity/aws/__init__.py
16
18
  src/velocity/aws/amplify.py
17
19
  src/velocity/aws/amplify_build.py
@@ -145,6 +147,7 @@ src/velocity_python.egg-info/requires.txt
145
147
  src/velocity_python.egg-info/top_level.txt
146
148
  tests/test_amplify_build.py
147
149
  tests/test_decorators.py
150
+ tests/test_formbuilder_template_validator.py
148
151
  tests/test_iconv_money_to_cents.py
149
152
  tests/test_lambda_handler.py
150
153
  tests/test_lambda_handler_auth.py
@@ -0,0 +1,240 @@
1
+ """Unit tests for velocity.app.validators.formbuilder_template.
2
+
3
+ These tests are the canonical regression suite for the shared validator.
4
+ Consumer-project tests (DonateMain, BackOffice) import from this module via
5
+ velocity so their suites should stay in sync.
6
+
7
+ Runs entirely offline — no database or network access required.
8
+
9
+ python -m pytest tests/test_formbuilder_template_validator.py -v
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import unittest
15
+
16
+ from velocity.app.validators.formbuilder_template import validate_formbuilder_template
17
+
18
+
19
+ # --------------------------------------------------------------------------- #
20
+ # Helpers
21
+ # --------------------------------------------------------------------------- #
22
+
23
+
24
+ def _make_valid_fb(**overrides) -> dict:
25
+ """Return the minimal template dict that passes all validations."""
26
+ base = {
27
+ "client": "TestClient",
28
+ "campaign": "test_campaign",
29
+ "url_slug": "/test/campaign",
30
+ "receipt_from_address": "noreply@example.com",
31
+ "receipt_subject": "Thank you for your donation",
32
+ "receipt_body_text": "<p>Dear {first_name}, thank you for donating!</p>",
33
+ "descriptor": "CaringCent*TEST",
34
+ "confirmation_header_text": "Thank you!",
35
+ "share_url": "https://donate.example.org/campaign",
36
+ "donations": {
37
+ "d1": {"label": "Give $25", "values": {"$25": "25"}},
38
+ },
39
+ }
40
+ base.update(overrides)
41
+ return base
42
+
43
+
44
+ def _errors(issues, field=None):
45
+ return [
46
+ i
47
+ for i in issues
48
+ if i["severity"] == "error" and (field is None or i["field"] == field)
49
+ ]
50
+
51
+
52
+ def _warnings(issues, field=None):
53
+ return [
54
+ i
55
+ for i in issues
56
+ if i["severity"] == "warning" and (field is None or i["field"] == field)
57
+ ]
58
+
59
+
60
+ # --------------------------------------------------------------------------- #
61
+ # Baseline
62
+ # --------------------------------------------------------------------------- #
63
+
64
+
65
+ class ValidTemplatePassesTests(unittest.TestCase):
66
+
67
+ def test_valid_template_has_no_issues(self):
68
+ issues = validate_formbuilder_template(_make_valid_fb())
69
+ self.assertEqual(issues, [], f"Unexpected issues: {issues}")
70
+
71
+ def test_returns_a_list(self):
72
+ result = validate_formbuilder_template(_make_valid_fb())
73
+ self.assertIsInstance(result, list)
74
+
75
+
76
+ # --------------------------------------------------------------------------- #
77
+ # Identity fields
78
+ # --------------------------------------------------------------------------- #
79
+
80
+
81
+ class IdentityFieldTests(unittest.TestCase):
82
+
83
+ def test_missing_client_is_error(self):
84
+ self.assertTrue(_errors(validate_formbuilder_template(_make_valid_fb(client="")), "client"))
85
+
86
+ def test_none_client_is_error(self):
87
+ self.assertTrue(_errors(validate_formbuilder_template(_make_valid_fb(client=None)), "client"))
88
+
89
+ def test_missing_campaign_is_error(self):
90
+ self.assertTrue(_errors(validate_formbuilder_template(_make_valid_fb(campaign="")), "campaign"))
91
+
92
+ def test_missing_url_slug_is_error(self):
93
+ fb = _make_valid_fb()
94
+ del fb["url_slug"]
95
+ self.assertTrue(_errors(validate_formbuilder_template(fb), "url_slug"))
96
+
97
+ def test_none_url_slug_is_error(self):
98
+ self.assertTrue(_errors(validate_formbuilder_template(_make_valid_fb(url_slug=None)), "url_slug"))
99
+
100
+
101
+ # --------------------------------------------------------------------------- #
102
+ # Receipt email fields
103
+ # --------------------------------------------------------------------------- #
104
+
105
+
106
+ class ReceiptEmailFieldTests(unittest.TestCase):
107
+
108
+ def test_empty_from_address_is_error(self):
109
+ self.assertTrue(_errors(validate_formbuilder_template(
110
+ _make_valid_fb(receipt_from_address="")), "receipt_from_address"))
111
+
112
+ def test_none_from_address_is_error(self):
113
+ self.assertTrue(_errors(validate_formbuilder_template(
114
+ _make_valid_fb(receipt_from_address=None)), "receipt_from_address"))
115
+
116
+ def test_invalid_from_address_is_error(self):
117
+ self.assertTrue(_errors(validate_formbuilder_template(
118
+ _make_valid_fb(receipt_from_address="not-an-email")), "receipt_from_address"))
119
+
120
+ def test_display_name_format_passes(self):
121
+ """'Display Name <user@example.com>' is RFC 5322 valid and used in practice."""
122
+ issues = validate_formbuilder_template(
123
+ _make_valid_fb(receipt_from_address="Tigers Athletics <noreply@example.com>"))
124
+ self.assertFalse(_errors(issues, "receipt_from_address"))
125
+
126
+ def test_empty_subject_is_warning(self):
127
+ self.assertTrue(_warnings(validate_formbuilder_template(
128
+ _make_valid_fb(receipt_subject="")), "receipt_subject"))
129
+
130
+ def test_empty_body_is_error(self):
131
+ self.assertTrue(_errors(validate_formbuilder_template(
132
+ _make_valid_fb(receipt_body_text="")), "receipt_body_text"))
133
+
134
+
135
+ # --------------------------------------------------------------------------- #
136
+ # Braintree descriptor
137
+ # --------------------------------------------------------------------------- #
138
+
139
+
140
+ class DescriptorTests(unittest.TestCase):
141
+
142
+ def test_valid_descriptor_passes(self):
143
+ issues = validate_formbuilder_template(_make_valid_fb())
144
+ self.assertFalse(_errors(issues, "descriptor"))
145
+ self.assertFalse(_warnings(issues, "descriptor"))
146
+
147
+ def test_omitted_descriptor_passes(self):
148
+ fb = _make_valid_fb()
149
+ del fb["descriptor"]
150
+ issues = validate_formbuilder_template(fb)
151
+ self.assertFalse(_errors(issues, "descriptor"))
152
+
153
+ def test_descriptor_too_long_is_error(self):
154
+ self.assertTrue(_errors(validate_formbuilder_template(
155
+ _make_valid_fb(descriptor="A" * 23)), "descriptor"))
156
+
157
+ def test_descriptor_exactly_22_chars_passes(self):
158
+ issues = validate_formbuilder_template(_make_valid_fb(descriptor="CaringCent*EXACTLY22CH"))
159
+ self.assertFalse(_errors(issues, "descriptor"))
160
+
161
+ def test_descriptor_without_asterisk_is_warning(self):
162
+ fb = _make_valid_fb(descriptor="CaringCentNOASTERISK")
163
+ issues = validate_formbuilder_template(fb)
164
+ self.assertTrue(_warnings(issues, "descriptor"))
165
+
166
+ def test_descriptor_with_non_ascii_is_error(self):
167
+ self.assertTrue(_errors(validate_formbuilder_template(
168
+ _make_valid_fb(descriptor="CaringCent*\x80TEST")), "descriptor"))
169
+
170
+
171
+ # --------------------------------------------------------------------------- #
172
+ # Content — donations / metrics
173
+ # --------------------------------------------------------------------------- #
174
+
175
+
176
+ class ContentConfigurationTests(unittest.TestCase):
177
+
178
+ def test_no_donations_no_metrics_is_warning(self):
179
+ fb = _make_valid_fb()
180
+ del fb["donations"]
181
+ self.assertTrue(_warnings(validate_formbuilder_template(fb), "donations/metrics"))
182
+
183
+ def test_modern_donations_dict_counts_as_content(self):
184
+ issues = validate_formbuilder_template(_make_valid_fb())
185
+ self.assertFalse(_warnings(issues, "donations/metrics"))
186
+
187
+ def test_hidden_donation_does_not_count_as_content(self):
188
+ fb = _make_valid_fb()
189
+ fb["donations"] = {"d1": {"label": "Hidden", "values": {}}}
190
+ fb["donation_d1_hide_donation_from_ui"] = True
191
+ issues = validate_formbuilder_template(fb)
192
+ self.assertTrue(_warnings(issues, "donations/metrics"))
193
+
194
+ def test_legacy_numbered_donation_fields_count_as_content(self):
195
+ fb = _make_valid_fb()
196
+ del fb["donations"]
197
+ fb["donation_1_label"] = "Give $25"
198
+ issues = validate_formbuilder_template(fb)
199
+ self.assertFalse(_warnings(issues, "donations/metrics"))
200
+
201
+
202
+ # --------------------------------------------------------------------------- #
203
+ # Share URL
204
+ # --------------------------------------------------------------------------- #
205
+
206
+
207
+ class ShareUrlTests(unittest.TestCase):
208
+
209
+ def test_missing_share_url_is_ok(self):
210
+ fb = _make_valid_fb()
211
+ del fb["share_url"]
212
+ self.assertFalse(_warnings(validate_formbuilder_template(fb), "share_url"))
213
+
214
+ def test_valid_https_url_passes(self):
215
+ issues = validate_formbuilder_template(_make_valid_fb())
216
+ self.assertFalse(_warnings(issues, "share_url"))
217
+
218
+ def test_non_url_share_url_is_warning(self):
219
+ self.assertTrue(_warnings(validate_formbuilder_template(
220
+ _make_valid_fb(share_url="not-a-url")), "share_url"))
221
+
222
+
223
+ # --------------------------------------------------------------------------- #
224
+ # Confirmation page text
225
+ # --------------------------------------------------------------------------- #
226
+
227
+
228
+ class ConfirmationTextTests(unittest.TestCase):
229
+
230
+ def test_empty_confirmation_header_is_warning(self):
231
+ self.assertTrue(_warnings(validate_formbuilder_template(
232
+ _make_valid_fb(confirmation_header_text="")), "confirmation_header_text"))
233
+
234
+ def test_valid_confirmation_header_passes(self):
235
+ issues = validate_formbuilder_template(_make_valid_fb())
236
+ self.assertFalse(_warnings(issues, "confirmation_header_text"))
237
+
238
+
239
+ if __name__ == "__main__":
240
+ unittest.main()