Skip to content

Commit 38ab85c

Browse files
committed
Add code-defined database queries for complex prefill sources
- Add database_query_key field to PrefillSource model - Add execute_custom_query method to DatabaseDataSource - Support dbquery.* prefix in prefill sources - Queries defined in settings.FORMS_WORKFLOWS_DATABASE_QUERIES - Enables complex SQL (JOINs, multiple WHERE conditions) without SQL injection concerns - Add migration 0013_add_database_query_key - Bump version to 0.7.7
1 parent 55346d4 commit 38ab85c

6 files changed

Lines changed: 122 additions & 3 deletions

File tree

django_forms_workflows/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Enterprise-grade, database-driven form builder with approval workflows
44
"""
55

6-
__version__ = "0.7.6"
6+
__version__ = "0.7.7"
77
__author__ = "Django Forms Workflows Contributors"
88
__license__ = "LGPL-3.0-only"
99

django_forms_workflows/data_sources/database_source.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,80 @@ def get_template_value(
384384
logger.error(f"Error getting template value: {e}")
385385
return None
386386

387+
def execute_custom_query(self, user, query_key: str) -> str | None:
388+
"""
389+
Execute a code-defined database query.
390+
391+
Queries are defined in settings.FORMS_WORKFLOWS_DATABASE_QUERIES
392+
and referenced by key. This allows complex queries (JOINs, multiple
393+
WHERE conditions) while keeping SQL in version-controlled code.
394+
395+
Args:
396+
user: Django User object
397+
query_key: Key from FORMS_WORKFLOWS_DATABASE_QUERIES setting
398+
399+
Returns:
400+
Query result (first column of first row) or None
401+
"""
402+
if not user or not user.is_authenticated:
403+
return None
404+
405+
try:
406+
# Get query definition from settings
407+
queries = getattr(settings, "FORMS_WORKFLOWS_DATABASE_QUERIES", {})
408+
if query_key not in queries:
409+
logger.error(
410+
f"Database query key '{query_key}' not found in "
411+
"FORMS_WORKFLOWS_DATABASE_QUERIES setting"
412+
)
413+
return None
414+
415+
query_config = queries[query_key]
416+
417+
# Extract configuration
418+
query_text = query_config.get("query")
419+
db_alias = query_config.get("db_alias")
420+
user_field = query_config.get("user_field", "employee_id")
421+
422+
if not query_text or not db_alias:
423+
logger.error(
424+
f"Database query '{query_key}' missing required 'query' or 'db_alias'"
425+
)
426+
return None
427+
428+
# Get user's lookup value
429+
user_id = self._get_user_id(user, user_field)
430+
if not user_id:
431+
logger.debug(f"User {user.username} has no {user_field}")
432+
return None
433+
434+
# Check database exists
435+
databases = getattr(settings, "DATABASES", {})
436+
if db_alias not in databases:
437+
logger.error(f"Database alias '{db_alias}' not configured")
438+
return None
439+
440+
# Execute the query
441+
with connections[db_alias].cursor() as cursor:
442+
cursor.execute(query_text, [user_id])
443+
row = cursor.fetchone()
444+
445+
if row:
446+
value = row[0]
447+
# Strip whitespace for fixed-width columns
448+
if isinstance(value, str):
449+
value = value.strip()
450+
return value
451+
452+
logger.debug(
453+
f"No data found for user {user_id} with query '{query_key}'"
454+
)
455+
return None
456+
457+
except Exception as e:
458+
logger.error(f"Error executing custom query '{query_key}': {e}")
459+
return None
460+
387461
def is_available(self) -> bool:
388462
"""
389463
Check if database source is configured.

django_forms_workflows/forms.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,8 @@ def _get_prefill_value(self, prefill_source, prefill_config=None):
300300
Supports:
301301
- user.* - User model fields
302302
- ldap.* - LDAP attributes
303-
- db.* or {{ db.* }} - Database queries
303+
- dbquery.* - Code-defined database queries (for complex SQL)
304+
- db.* or {{ db.* }} - Simple database queries
304305
- api.* - API calls
305306
- current_date, current_datetime - Current date/time
306307
- last_submission - Previous submission data
@@ -325,6 +326,12 @@ def _get_prefill_value(self, prefill_source, prefill_config=None):
325326
field_name = prefill_source.replace("ldap.", "")
326327
return source.get_value(self.user, field_name) or ""
327328

329+
# Handle dbquery.* sources (code-defined complex queries)
330+
elif prefill_source.startswith("dbquery."):
331+
source = DatabaseDataSource()
332+
query_key = prefill_source.replace("dbquery.", "")
333+
return source.execute_custom_query(self.user, query_key) or ""
334+
328335
# Handle db.* or {{ db.* }} sources
329336
elif prefill_source.startswith("db.") or prefill_source.startswith("{{"):
330337
source = DatabaseDataSource()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 5.2.7 on 2026-01-22 00:08
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("django_forms_workflows", "0012_add_approval_step_fields"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="prefillsource",
15+
name="database_query_key",
16+
field=models.CharField(
17+
blank=True,
18+
help_text="Key from FORMS_WORKFLOWS_DATABASE_QUERIES setting. Use this for complex queries (JOINs, additional WHERE conditions). Takes precedence over db_schema/db_table/db_column fields.",
19+
max_length=100,
20+
),
21+
),
22+
]

django_forms_workflows/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,15 @@ class PrefillSource(models.Model):
201201
blank=True, null=True, help_text="Additional configuration as JSON"
202202
)
203203

204+
# Code-defined database query (for complex queries like JOINs)
205+
database_query_key = models.CharField(
206+
max_length=100,
207+
blank=True,
208+
help_text="Key from FORMS_WORKFLOWS_DATABASE_QUERIES setting. "
209+
"Use this for complex queries (JOINs, additional WHERE conditions). "
210+
"Takes precedence over db_schema/db_table/db_column fields.",
211+
)
212+
204213
# Metadata
205214
created_at = models.DateTimeField(auto_now_add=True)
206215
updated_at = models.DateTimeField(auto_now=True)
@@ -219,6 +228,9 @@ def get_source_identifier(self):
219228
Get the source identifier string for backward compatibility.
220229
Returns the source_key or constructs it from components.
221230
"""
231+
# Code-defined database query takes precedence
232+
if self.source_type == "database" and self.database_query_key:
233+
return f"dbquery.{self.database_query_key}"
222234
if self.source_type == "database" and self.db_schema and self.db_table:
223235
# Template-based multi-column lookup
224236
if self.db_template and self.db_columns:
@@ -232,6 +244,10 @@ def get_source_identifier(self):
232244
return self.source_key
233245
return self.source_key
234246

247+
def has_custom_query(self):
248+
"""Check if this source uses a code-defined database query."""
249+
return bool(self.database_query_key)
250+
235251
def has_template(self):
236252
"""Check if this source uses a multi-column template."""
237253
return bool(self.db_template and self.db_columns)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-forms-workflows"
3-
version = "0.7.6"
3+
version = "0.7.7"
44
description = "Enterprise-grade, database-driven form builder with approval workflows and external data integration"
55
license = "LGPL-3.0-only"
66
readme = "README.md"

0 commit comments

Comments
 (0)