Skip to content

Commit f696c64

Browse files
committed
Additional LDAP groups improvements
1 parent 5b6d353 commit f696c64

File tree

12 files changed

+988
-6
lines changed

12 files changed

+988
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
dist
22
*.pyc$
33
db.sqlite3
4+
form-workflows/
45
__pycache__/

CHANGELOG.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Advanced reporting and analytics
1616
- Multi-tenancy support
1717

18+
## [0.4.0] - 2025-11-06
19+
20+
### Added - SJCME Migration Support
21+
- **Enhanced UserProfile Model**
22+
- Added `ldap_last_sync` timestamp field for tracking LDAP synchronization
23+
- Added database indexes to `employee_id` and `external_id` fields for better performance
24+
- Added `id_number` property as backward-compatible alias for `employee_id`
25+
- Added `full_name` and `display_name` properties for convenient user display
26+
- Enhanced help text for LDAP attribute fields
27+
28+
- **LDAP Integration Enhancements**
29+
- New `signals.py` module with automatic LDAP attribute synchronization
30+
- `sync_ldap_attributes()` function for syncing LDAP data to UserProfile
31+
- `get_ldap_attribute()` helper function for retrieving LDAP attributes
32+
- Auto-sync on user login (configurable via `FORMS_WORKFLOWS['LDAP_SYNC']`)
33+
- Signal handlers for automatic UserProfile creation on user creation
34+
- Configurable LDAP attribute mappings in settings
35+
36+
- **Database Introspection Utilities**
37+
- `DatabaseDataSource.test_connection()` - Test external database connections
38+
- `DatabaseDataSource.get_available_tables()` - List tables in a schema
39+
- `DatabaseDataSource.get_table_columns()` - Get column information for a table
40+
- Support for SQL Server, PostgreSQL, MySQL, and SQLite introspection
41+
42+
- **Utility Functions**
43+
- `get_user_manager()` - Get user's manager from LDAP or UserProfile
44+
- `user_can_view_form()` - Check if user can view a form
45+
- `user_can_view_submission()` - Check if user can view a submission
46+
- `check_escalation_needed()` - Check if submission needs escalation
47+
- `sync_ldap_groups()` - Synchronize LDAP groups to Django groups
48+
49+
- **Management Commands**
50+
- `sync_ldap_profiles` - Bulk sync LDAP attributes for all users
51+
- Supports `--username` for single user sync
52+
- Supports `--dry-run` for testing without changes
53+
- Supports `--verbose` for detailed output
54+
- `test_db_connection` - Test external database connections
55+
- Supports `--database` to specify database alias
56+
- Supports `--verbose` for detailed connection information
57+
- Works with SQL Server, PostgreSQL, MySQL, and SQLite
58+
59+
- **Documentation**
60+
- `PORTING_ANALYSIS.md` - Detailed analysis of SJCME to package migration
61+
- `FEATURE_COMPARISON.md` - Comprehensive feature comparison matrix
62+
- `SJCME_SIMPLIFICATION_PLAN.md` - Code reduction and migration strategy
63+
- `EXECUTIVE_SUMMARY.md` - High-level overview for stakeholders
64+
- `NEXT_STEPS.md` - Actionable implementation guide
65+
66+
### Changed
67+
- Updated version to 0.4.0 to reflect significant new features
68+
- Enhanced UserProfile model with LDAP-specific fields and properties
69+
- Improved database source with introspection capabilities
70+
- Expanded utility functions for better LDAP and permission handling
71+
72+
### Migration Notes
73+
- Run `python manage.py migrate django_forms_workflows` to apply UserProfile enhancements
74+
- Configure LDAP sync in settings:
75+
```python
76+
FORMS_WORKFLOWS = {
77+
'LDAP_SYNC': {
78+
'enabled': True,
79+
'sync_on_login': True,
80+
'attributes': {
81+
'employee_id': 'extensionAttribute1',
82+
'department': 'department',
83+
'title': 'title',
84+
'phone': 'telephoneNumber',
85+
'manager_dn': 'manager',
86+
}
87+
}
88+
}
89+
```
90+
- Use `python manage.py sync_ldap_profiles` to bulk sync existing users
91+
1892
## [0.2.2] - 2025-10-31
1993

2094
### Changed

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.3.0"
6+
__version__ = "0.4.0"
77
__author__ = "Django Forms Workflows Contributors"
88
__license__ = "LGPL-3.0-only"
99

django_forms_workflows/apps.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,5 @@ def ready(self):
1616
"""
1717
Import signal handlers and perform app initialization.
1818
"""
19-
# Import signals if you have any
20-
# from . import signals
21-
pass
19+
# Import signals to register handlers
20+
from . import signals # noqa: F401

django_forms_workflows/data_sources/database_source.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,170 @@ def is_available(self) -> bool:
256256

257257
def get_display_name(self) -> str:
258258
return "External Database"
259+
260+
def test_connection(self, database_alias: str = None) -> bool:
261+
"""
262+
Test the database connection.
263+
264+
Args:
265+
database_alias: Database alias to test (uses config default if not provided)
266+
267+
Returns:
268+
True if connection successful, False otherwise
269+
"""
270+
if database_alias is None:
271+
config = self._get_config()
272+
database_alias = config.get("database_alias")
273+
274+
if not database_alias:
275+
logger.error("No database alias configured")
276+
return False
277+
278+
try:
279+
with connections[database_alias].cursor() as cursor:
280+
# Try to get database version
281+
engine = connections[database_alias].settings_dict.get("ENGINE", "")
282+
283+
if "mssql" in engine or "sql_server" in engine:
284+
cursor.execute("SELECT @@VERSION")
285+
elif "postgresql" in engine or "postgis" in engine:
286+
cursor.execute("SELECT version()")
287+
elif "mysql" in engine:
288+
cursor.execute("SELECT VERSION()")
289+
elif "sqlite" in engine:
290+
cursor.execute("SELECT sqlite_version()")
291+
else:
292+
cursor.execute("SELECT 1")
293+
294+
result = cursor.fetchone()
295+
if result:
296+
logger.info(
297+
f"Database connection successful for '{database_alias}'"
298+
)
299+
return True
300+
301+
return False
302+
303+
except Exception as e:
304+
logger.error(f"Database connection test failed for '{database_alias}': {e}")
305+
return False
306+
307+
def get_available_tables(
308+
self, schema: str = None, database_alias: str = None
309+
) -> list:
310+
"""
311+
Get list of available tables in a schema.
312+
313+
Args:
314+
schema: Schema name (uses config default if not provided)
315+
database_alias: Database alias (uses config default if not provided)
316+
317+
Returns:
318+
List of table names
319+
"""
320+
config = self._get_config()
321+
322+
if schema is None:
323+
schema = config.get("default_schema", "dbo")
324+
325+
if database_alias is None:
326+
database_alias = config.get("database_alias")
327+
328+
if not database_alias:
329+
logger.error("No database alias configured")
330+
return []
331+
332+
if not self._is_safe_identifier(schema):
333+
logger.error(f"Invalid schema name: {schema}")
334+
return []
335+
336+
try:
337+
query = """
338+
SELECT TABLE_NAME
339+
FROM INFORMATION_SCHEMA.TABLES
340+
WHERE TABLE_SCHEMA = %s
341+
AND TABLE_TYPE = 'BASE TABLE'
342+
ORDER BY TABLE_NAME
343+
"""
344+
345+
with connections[database_alias].cursor() as cursor:
346+
cursor.execute(query, [schema])
347+
rows = cursor.fetchall()
348+
return [row[0] for row in rows]
349+
350+
except Exception as e:
351+
logger.error(f"Error getting tables from {schema}: {e}")
352+
return []
353+
354+
def get_table_columns(
355+
self, table: str, schema: str = None, database_alias: str = None
356+
) -> list:
357+
"""
358+
Get list of columns in a table.
359+
360+
Args:
361+
table: Table name
362+
schema: Schema name (uses config default if not provided)
363+
database_alias: Database alias (uses config default if not provided)
364+
365+
Returns:
366+
List of dictionaries with column information:
367+
[
368+
{
369+
'name': 'COLUMN_NAME',
370+
'type': 'DATA_TYPE',
371+
'max_length': 100,
372+
'nullable': True
373+
},
374+
...
375+
]
376+
"""
377+
config = self._get_config()
378+
379+
if schema is None:
380+
schema = config.get("default_schema", "dbo")
381+
382+
if database_alias is None:
383+
database_alias = config.get("database_alias")
384+
385+
if not database_alias:
386+
logger.error("No database alias configured")
387+
return []
388+
389+
if not self._is_safe_identifier(schema) or not self._is_safe_identifier(table):
390+
logger.error(f"Invalid schema or table name: {schema}.{table}")
391+
return []
392+
393+
try:
394+
query = """
395+
SELECT
396+
COLUMN_NAME,
397+
DATA_TYPE,
398+
CHARACTER_MAXIMUM_LENGTH,
399+
IS_NULLABLE
400+
FROM INFORMATION_SCHEMA.COLUMNS
401+
WHERE TABLE_SCHEMA = %s
402+
AND TABLE_NAME = %s
403+
ORDER BY ORDINAL_POSITION
404+
"""
405+
406+
with connections[database_alias].cursor() as cursor:
407+
cursor.execute(query, [schema, table])
408+
rows = cursor.fetchall()
409+
410+
columns = []
411+
for row in rows:
412+
columns.append(
413+
{
414+
"name": row[0],
415+
"type": row[1],
416+
"max_length": row[2],
417+
"nullable": row[3] == "YES",
418+
}
419+
)
420+
421+
return columns
422+
423+
except Exception as e:
424+
logger.error(f"Error getting columns from {schema}.{table}: {e}")
425+
return []

0 commit comments

Comments
 (0)