django-forms-workflows 0.2.0__py3-none-any.whl

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 (48) hide show
  1. django_forms_workflows/__init__.py +11 -0
  2. django_forms_workflows/admin.py +434 -0
  3. django_forms_workflows/apps.py +22 -0
  4. django_forms_workflows/data_sources/__init__.py +79 -0
  5. django_forms_workflows/data_sources/base.py +119 -0
  6. django_forms_workflows/data_sources/database_source.py +242 -0
  7. django_forms_workflows/data_sources/ldap_source.py +147 -0
  8. django_forms_workflows/data_sources/user_source.py +73 -0
  9. django_forms_workflows/forms.py +359 -0
  10. django_forms_workflows/handlers/__init__.py +20 -0
  11. django_forms_workflows/handlers/api_handler.py +205 -0
  12. django_forms_workflows/handlers/base.py +90 -0
  13. django_forms_workflows/handlers/database_handler.py +201 -0
  14. django_forms_workflows/handlers/executor.py +272 -0
  15. django_forms_workflows/handlers/ldap_handler.py +228 -0
  16. django_forms_workflows/ldap_backend.py +424 -0
  17. django_forms_workflows/management/commands/seed_farm_demo.py +377 -0
  18. django_forms_workflows/management/commands/seed_prefill_sources.py +162 -0
  19. django_forms_workflows/migrations/0001_initial.py +755 -0
  20. django_forms_workflows/migrations/0002_prefillsource_alter_formfield_prefill_source_and_more.py +185 -0
  21. django_forms_workflows/migrations/0003_postsubmissionaction.py +252 -0
  22. django_forms_workflows/migrations/__init__.py +0 -0
  23. django_forms_workflows/models.py +1025 -0
  24. django_forms_workflows/tasks.py +263 -0
  25. django_forms_workflows/templates/django_forms_workflows/approval_inbox.html +52 -0
  26. django_forms_workflows/templates/django_forms_workflows/approve.html +74 -0
  27. django_forms_workflows/templates/django_forms_workflows/base.html +119 -0
  28. django_forms_workflows/templates/django_forms_workflows/form_list.html +38 -0
  29. django_forms_workflows/templates/django_forms_workflows/form_submit.html +34 -0
  30. django_forms_workflows/templates/django_forms_workflows/my_submissions.html +80 -0
  31. django_forms_workflows/templates/django_forms_workflows/submission_detail.html +148 -0
  32. django_forms_workflows/templates/django_forms_workflows/withdraw_confirm.html +38 -0
  33. django_forms_workflows/templates/emails/approval_notification.html +29 -0
  34. django_forms_workflows/templates/emails/approval_reminder.html +29 -0
  35. django_forms_workflows/templates/emails/approval_request.html +42 -0
  36. django_forms_workflows/templates/emails/email_styles.html +10 -0
  37. django_forms_workflows/templates/emails/escalation_notification.html +28 -0
  38. django_forms_workflows/templates/emails/rejection_notification.html +28 -0
  39. django_forms_workflows/templates/emails/submission_notification.html +29 -0
  40. django_forms_workflows/templates/registration/login.html +63 -0
  41. django_forms_workflows/urls.py +20 -0
  42. django_forms_workflows/utils.py +46 -0
  43. django_forms_workflows/views.py +358 -0
  44. django_forms_workflows/workflow_engine.py +290 -0
  45. django_forms_workflows-0.2.0.dist-info/LICENSE +165 -0
  46. django_forms_workflows-0.2.0.dist-info/METADATA +331 -0
  47. django_forms_workflows-0.2.0.dist-info/RECORD +48 -0
  48. django_forms_workflows-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,11 @@
1
+ """
2
+ Django Forms Workflows
3
+ Enterprise-grade, database-driven form builder with approval workflows
4
+ """
5
+
6
+ __version__ = '0.2.0'
7
+ __author__ = 'Django Forms Workflows Contributors'
8
+ __license__ = 'LGPL-3.0-only'
9
+
10
+ default_app_config = 'django_forms_workflows.apps.DjangoFormsWorkflowsConfig'
11
+
@@ -0,0 +1,434 @@
1
+ """
2
+ Django admin for Django Forms Workflows
3
+
4
+ Provides a friendly admin interface to build forms (with fields),
5
+ configure approval workflows, and review submissions and audit logs.
6
+ """
7
+ from django.contrib import admin
8
+ from django.utils.html import format_html
9
+ from .models import (
10
+ FormDefinition,
11
+ FormField,
12
+ PrefillSource,
13
+ PostSubmissionAction,
14
+ WorkflowDefinition,
15
+ FormSubmission,
16
+ ApprovalTask,
17
+ AuditLog,
18
+ UserProfile,
19
+ )
20
+
21
+
22
+ # Inline for form fields when editing a form definition
23
+ class FormFieldInline(admin.StackedInline):
24
+ model = FormField
25
+ extra = 0
26
+ ordering = ("order",)
27
+ fk_name = "form_definition"
28
+ fieldsets = (
29
+ (
30
+ None,
31
+ {
32
+ "fields": (
33
+ ("order", "field_label", "field_name", "field_type", "required"),
34
+ ("help_text", "placeholder", "width", "css_class"),
35
+ )
36
+ },
37
+ ),
38
+ (
39
+ "Validation",
40
+ {
41
+ "classes": ("collapse",),
42
+ "fields": (
43
+ ("min_value", "max_value"),
44
+ ("min_length", "max_length"),
45
+ "regex_validation",
46
+ "regex_error_message",
47
+ ),
48
+ },
49
+ ),
50
+ (
51
+ "Choices & Defaults",
52
+ {
53
+ "classes": ("collapse",),
54
+ "fields": ("choices", "prefill_source_config", "prefill_source", "default_value"),
55
+ },
56
+ ),
57
+ (
58
+ "Conditional display",
59
+ {
60
+ "classes": ("collapse",),
61
+ "fields": (("show_if_field", "show_if_value"),),
62
+ },
63
+ ),
64
+ (
65
+ "File upload",
66
+ {
67
+ "classes": ("collapse",),
68
+ "fields": ("allowed_extensions", "max_file_size_mb"),
69
+ },
70
+ ),
71
+ )
72
+
73
+
74
+ @admin.register(PrefillSource)
75
+ class PrefillSourceAdmin(admin.ModelAdmin):
76
+ list_display = (
77
+ "name",
78
+ "source_type",
79
+ "source_key",
80
+ "is_active",
81
+ "order",
82
+ )
83
+ list_filter = ("source_type", "is_active")
84
+ search_fields = ("name", "source_key", "description")
85
+ list_editable = ("order", "is_active")
86
+ fieldsets = (
87
+ (
88
+ None,
89
+ {
90
+ "fields": (
91
+ ("name", "source_type"),
92
+ "source_key",
93
+ "description",
94
+ ("is_active", "order"),
95
+ )
96
+ },
97
+ ),
98
+ (
99
+ "Database Configuration",
100
+ {
101
+ "classes": ("collapse",),
102
+ "fields": (
103
+ "db_alias",
104
+ ("db_schema", "db_table", "db_column"),
105
+ ("db_lookup_field", "db_user_field"),
106
+ ),
107
+ },
108
+ ),
109
+ (
110
+ "LDAP Configuration",
111
+ {
112
+ "classes": ("collapse",),
113
+ "fields": ("ldap_attribute",),
114
+ },
115
+ ),
116
+ (
117
+ "API Configuration",
118
+ {
119
+ "classes": ("collapse",),
120
+ "fields": ("api_endpoint", "api_field"),
121
+ },
122
+ ),
123
+ (
124
+ "Custom Configuration",
125
+ {
126
+ "classes": ("collapse",),
127
+ "fields": ("custom_config",),
128
+ },
129
+ ),
130
+ )
131
+
132
+
133
+ @admin.register(FormDefinition)
134
+ class FormDefinitionAdmin(admin.ModelAdmin):
135
+ list_display = (
136
+ "name",
137
+ "slug",
138
+ "is_active",
139
+ "requires_login",
140
+ "version",
141
+ "created_at",
142
+ )
143
+ list_filter = ("is_active", "requires_login")
144
+ search_fields = ("name", "slug", "description")
145
+ prepopulated_fields = {"slug": ("name",)}
146
+ inlines = [FormFieldInline]
147
+ filter_horizontal = ("submit_groups", "view_groups", "admin_groups")
148
+
149
+
150
+ @admin.register(WorkflowDefinition)
151
+ class WorkflowDefinitionAdmin(admin.ModelAdmin):
152
+ list_display = (
153
+ "form_definition",
154
+ "requires_approval",
155
+ "approval_logic",
156
+ "requires_manager_approval",
157
+ )
158
+ list_filter = (
159
+ "requires_approval",
160
+ "approval_logic",
161
+ "requires_manager_approval",
162
+ )
163
+ search_fields = ("form_definition__name",)
164
+ filter_horizontal = ("approval_groups", "escalation_groups")
165
+ fieldsets = (
166
+ (
167
+ None,
168
+ {
169
+ "fields": (
170
+ "form_definition",
171
+ ("requires_approval", "approval_logic"),
172
+ "approval_groups",
173
+ )
174
+ },
175
+ ),
176
+ (
177
+ "Manager approval",
178
+ {
179
+ "classes": ("collapse",),
180
+ "fields": ("requires_manager_approval", "manager_can_override_group"),
181
+ },
182
+ ),
183
+ (
184
+ "Conditional escalation",
185
+ {
186
+ "classes": ("collapse",),
187
+ "fields": (
188
+ ("escalation_field", "escalation_threshold"),
189
+ "escalation_groups",
190
+ ),
191
+ },
192
+ ),
193
+ (
194
+ "Timeouts",
195
+ {
196
+ "classes": ("collapse",),
197
+ "fields": (
198
+ "approval_deadline_days",
199
+ "send_reminder_after_days",
200
+ "auto_approve_after_days",
201
+ ),
202
+ },
203
+ ),
204
+ (
205
+ "Notifications",
206
+ {
207
+ "classes": ("collapse",),
208
+ "fields": (
209
+ (
210
+ "notify_on_submission",
211
+ "notify_on_approval",
212
+ "notify_on_rejection",
213
+ "notify_on_withdrawal",
214
+ ),
215
+ "additional_notify_emails",
216
+ ),
217
+ },
218
+ ),
219
+ (
220
+ "Post-approval DB updates",
221
+ {
222
+ "classes": ("collapse",),
223
+ "fields": ("enable_db_updates", "db_update_mappings"),
224
+ },
225
+ ),
226
+ )
227
+
228
+
229
+ @admin.register(PostSubmissionAction)
230
+ class PostSubmissionActionAdmin(admin.ModelAdmin):
231
+ list_display = (
232
+ "name",
233
+ "form_definition",
234
+ "action_type",
235
+ "trigger",
236
+ "is_active",
237
+ "order",
238
+ )
239
+ list_filter = (
240
+ "action_type",
241
+ "trigger",
242
+ "is_active",
243
+ "form_definition",
244
+ )
245
+ search_fields = (
246
+ "name",
247
+ "description",
248
+ "form_definition__name",
249
+ )
250
+ list_editable = ("is_active", "order")
251
+ ordering = ("form_definition", "order", "name")
252
+
253
+ fieldsets = (
254
+ (
255
+ None,
256
+ {
257
+ "fields": (
258
+ "form_definition",
259
+ "name",
260
+ "description",
261
+ ("action_type", "trigger"),
262
+ ("is_active", "order"),
263
+ )
264
+ },
265
+ ),
266
+ (
267
+ "Database Update Configuration",
268
+ {
269
+ "classes": ("collapse",),
270
+ "fields": (
271
+ ("db_alias", "db_schema", "db_table"),
272
+ ("db_lookup_field", "db_user_field"),
273
+ "db_field_mappings",
274
+ ),
275
+ "description": (
276
+ "Configure database updates. Field mappings format: "
277
+ '[{"form_field": "email", "db_column": "EMAIL_ADDRESS"}, ...]'
278
+ ),
279
+ },
280
+ ),
281
+ (
282
+ "LDAP Update Configuration",
283
+ {
284
+ "classes": ("collapse",),
285
+ "fields": (
286
+ "ldap_dn_template",
287
+ "ldap_field_mappings",
288
+ ),
289
+ "description": (
290
+ "Configure LDAP updates. Field mappings format: "
291
+ '[{"form_field": "phone", "ldap_attribute": "telephoneNumber"}, ...]'
292
+ ),
293
+ },
294
+ ),
295
+ (
296
+ "API Call Configuration",
297
+ {
298
+ "classes": ("collapse",),
299
+ "fields": (
300
+ ("api_endpoint", "api_method"),
301
+ "api_headers",
302
+ "api_body_template",
303
+ ),
304
+ "description": (
305
+ "Configure API calls. Use {field_name} in body template for form field values."
306
+ ),
307
+ },
308
+ ),
309
+ (
310
+ "Custom Handler Configuration",
311
+ {
312
+ "classes": ("collapse",),
313
+ "fields": (
314
+ "custom_handler_path",
315
+ "custom_handler_config",
316
+ ),
317
+ "description": (
318
+ "Python path to custom handler function (e.g., 'myapp.handlers.custom_update')"
319
+ ),
320
+ },
321
+ ),
322
+ (
323
+ "Conditional Execution",
324
+ {
325
+ "classes": ("collapse",),
326
+ "fields": (
327
+ "condition_field",
328
+ ("condition_operator", "condition_value"),
329
+ ),
330
+ "description": (
331
+ "Execute this action only when the condition is met."
332
+ ),
333
+ },
334
+ ),
335
+ (
336
+ "Error Handling",
337
+ {
338
+ "classes": ("collapse",),
339
+ "fields": (
340
+ "fail_silently",
341
+ ("retry_on_failure", "max_retries"),
342
+ ),
343
+ },
344
+ ),
345
+ (
346
+ "Metadata",
347
+ {
348
+ "classes": ("collapse",),
349
+ "fields": (
350
+ "created_at",
351
+ "updated_at",
352
+ ),
353
+ },
354
+ ),
355
+ )
356
+
357
+ readonly_fields = ("created_at", "updated_at")
358
+
359
+
360
+ @admin.register(FormSubmission)
361
+ class FormSubmissionAdmin(admin.ModelAdmin):
362
+ list_display = (
363
+ "id",
364
+ "form_definition",
365
+ "submitter",
366
+ "status",
367
+ "created_at",
368
+ "submitted_at",
369
+ "completed_at",
370
+ )
371
+ list_filter = ("status", "form_definition")
372
+ date_hierarchy = "created_at"
373
+ search_fields = (
374
+ "id",
375
+ "form_definition__name",
376
+ "submitter__username",
377
+ "submitter__email",
378
+ )
379
+ raw_id_fields = ("submitter",)
380
+ readonly_fields = ("created_at", "submitted_at", "completed_at")
381
+
382
+
383
+ @admin.register(ApprovalTask)
384
+ class ApprovalTaskAdmin(admin.ModelAdmin):
385
+ list_display = (
386
+ "id",
387
+ "submission",
388
+ "step_name",
389
+ "status",
390
+ "assigned_to",
391
+ "assigned_group",
392
+ "due_date",
393
+ "completed_at",
394
+ )
395
+ list_filter = ("status", "step_name", "assigned_group")
396
+ search_fields = (
397
+ "submission__id",
398
+ "submission__form_definition__name",
399
+ "assigned_to__username",
400
+ )
401
+ raw_id_fields = ("submission", "assigned_to", "completed_by")
402
+ readonly_fields = ("created_at", "reminder_sent_at")
403
+
404
+
405
+ @admin.register(AuditLog)
406
+ class AuditLogAdmin(admin.ModelAdmin):
407
+ list_display = ("created_at", "user", "action", "object_type", "object_id")
408
+ list_filter = ("action", "object_type")
409
+ date_hierarchy = "created_at"
410
+ search_fields = (
411
+ "user__username",
412
+ "object_type",
413
+ "object_id",
414
+ "comments",
415
+ )
416
+ readonly_fields = (
417
+ "created_at",
418
+ "user",
419
+ "action",
420
+ "object_type",
421
+ "object_id",
422
+ "user_ip",
423
+ "changes",
424
+ "comments",
425
+ )
426
+
427
+
428
+ @admin.register(UserProfile)
429
+ class UserProfileAdmin(admin.ModelAdmin):
430
+ list_display = ("user", "department", "title", "employee_id")
431
+ search_fields = ("user__username", "user__email", "department", "title")
432
+ raw_id_fields = ("user", "manager")
433
+ list_filter = ("department",)
434
+
@@ -0,0 +1,22 @@
1
+ """
2
+ Django Forms Workflows App Configuration
3
+ """
4
+
5
+ from django.apps import AppConfig
6
+
7
+
8
+ class DjangoFormsWorkflowsConfig(AppConfig):
9
+ """App configuration for Django Forms Workflows"""
10
+
11
+ default_auto_field = 'django.db.models.BigAutoField'
12
+ name = 'django_forms_workflows'
13
+ verbose_name = 'Forms Workflows'
14
+
15
+ def ready(self):
16
+ """
17
+ Import signal handlers and perform app initialization.
18
+ """
19
+ # Import signals if you have any
20
+ # from . import signals
21
+ pass
22
+
@@ -0,0 +1,79 @@
1
+ """
2
+ Data Source Abstraction Layer
3
+
4
+ Pluggable system for prefilling form fields from external sources:
5
+ - LDAP/Active Directory
6
+ - External databases
7
+ - REST APIs
8
+ - Custom sources
9
+
10
+ Usage:
11
+ from django_forms_workflows.data_sources import get_data_source
12
+
13
+ source = get_data_source('ldap')
14
+ value = source.get_value(user, 'department')
15
+ """
16
+
17
+ from .base import DataSource, DataSourceRegistry
18
+ from .user_source import UserDataSource
19
+ from .ldap_source import LDAPDataSource
20
+ from .database_source import DatabaseDataSource
21
+
22
+ # Global registry
23
+ registry = DataSourceRegistry()
24
+
25
+ # Register built-in sources
26
+ registry.register('user', UserDataSource)
27
+ registry.register('ldap', LDAPDataSource)
28
+ registry.register('database', DatabaseDataSource)
29
+ registry.register('db', DatabaseDataSource) # Alias
30
+
31
+
32
+ def get_data_source(source_type):
33
+ """
34
+ Get a data source instance by type.
35
+
36
+ Args:
37
+ source_type: Type of data source ('user', 'ldap', 'database', etc.)
38
+
39
+ Returns:
40
+ DataSource instance
41
+
42
+ Raises:
43
+ ValueError: If source type is not registered
44
+ """
45
+ return registry.get(source_type)
46
+
47
+
48
+ def register_data_source(source_type, source_class):
49
+ """
50
+ Register a custom data source.
51
+
52
+ Args:
53
+ source_type: Unique identifier for the source
54
+ source_class: DataSource subclass
55
+
56
+ Example:
57
+ from django_forms_workflows.data_sources import register_data_source, DataSource
58
+
59
+ class SalesforceSource(DataSource):
60
+ def get_value(self, user, field_name, **kwargs):
61
+ # Query Salesforce API
62
+ pass
63
+
64
+ register_data_source('salesforce', SalesforceSource)
65
+ """
66
+ registry.register(source_type, source_class)
67
+
68
+
69
+ __all__ = [
70
+ 'DataSource',
71
+ 'DataSourceRegistry',
72
+ 'UserDataSource',
73
+ 'LDAPDataSource',
74
+ 'DatabaseDataSource',
75
+ 'get_data_source',
76
+ 'register_data_source',
77
+ 'registry',
78
+ ]
79
+
@@ -0,0 +1,119 @@
1
+ """
2
+ Base classes for data source abstraction
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Optional, Dict
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class DataSource(ABC):
13
+ """
14
+ Abstract base class for data sources.
15
+
16
+ All data sources must implement the get_value method.
17
+ """
18
+
19
+ @abstractmethod
20
+ def get_value(self, user, field_name: str, **kwargs) -> Optional[Any]:
21
+ """
22
+ Get a value from the data source.
23
+
24
+ Args:
25
+ user: Django User object
26
+ field_name: Name of the field to retrieve
27
+ **kwargs: Additional parameters (e.g., schema, table, column)
28
+
29
+ Returns:
30
+ The field value, or None if not found
31
+ """
32
+ pass
33
+
34
+ def is_available(self) -> bool:
35
+ """
36
+ Check if this data source is available/configured.
37
+
38
+ Returns:
39
+ True if the data source can be used, False otherwise
40
+ """
41
+ return True
42
+
43
+ def get_display_name(self) -> str:
44
+ """
45
+ Get a human-readable name for this data source.
46
+
47
+ Returns:
48
+ Display name
49
+ """
50
+ return self.__class__.__name__
51
+
52
+
53
+ class DataSourceRegistry:
54
+ """
55
+ Registry for data sources.
56
+
57
+ Allows registration and retrieval of data source implementations.
58
+ """
59
+
60
+ def __init__(self):
61
+ self._sources: Dict[str, type] = {}
62
+
63
+ def register(self, source_type: str, source_class: type):
64
+ """
65
+ Register a data source.
66
+
67
+ Args:
68
+ source_type: Unique identifier for the source
69
+ source_class: DataSource subclass
70
+ """
71
+ if not issubclass(source_class, DataSource):
72
+ raise ValueError(f"{source_class} must be a subclass of DataSource")
73
+
74
+ self._sources[source_type] = source_class
75
+ logger.info(f"Registered data source: {source_type} -> {source_class.__name__}")
76
+
77
+ def get(self, source_type: str) -> DataSource:
78
+ """
79
+ Get a data source instance by type.
80
+
81
+ Args:
82
+ source_type: Type of data source
83
+
84
+ Returns:
85
+ DataSource instance
86
+
87
+ Raises:
88
+ ValueError: If source type is not registered
89
+ """
90
+ if source_type not in self._sources:
91
+ raise ValueError(
92
+ f"Unknown data source type: {source_type}. "
93
+ f"Available types: {', '.join(self._sources.keys())}"
94
+ )
95
+
96
+ source_class = self._sources[source_type]
97
+ return source_class()
98
+
99
+ def list_sources(self) -> list:
100
+ """
101
+ List all registered data source types.
102
+
103
+ Returns:
104
+ List of source type names
105
+ """
106
+ return list(self._sources.keys())
107
+
108
+ def is_registered(self, source_type: str) -> bool:
109
+ """
110
+ Check if a source type is registered.
111
+
112
+ Args:
113
+ source_type: Type to check
114
+
115
+ Returns:
116
+ True if registered, False otherwise
117
+ """
118
+ return source_type in self._sources
119
+