BmorricalDjangoWorkflows 2.1.2__tar.gz → 2.1.4__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.
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/BmorricalDjangoWorkflows.egg-info/PKG-INFO +15 -1
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/PKG-INFO +15 -1
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/README.md +14 -0
- bmorricaldjangoworkflows-2.1.4/VERSION +1 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/users/services/auth_flow.py +36 -10
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/users/views.py +2 -2
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/tests/test_auth_flow.py +56 -2
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/tests/test_user_views.py +0 -2
- bmorricaldjangoworkflows-2.1.2/VERSION +0 -1
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/BmorricalDjangoWorkflows.egg-info/SOURCES.txt +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/BmorricalDjangoWorkflows.egg-info/dependency_links.txt +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/BmorricalDjangoWorkflows.egg-info/requires.txt +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/BmorricalDjangoWorkflows.egg-info/top_level.txt +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/LICENSE +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/MANIFEST.in +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/services/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/services/email.py +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/users/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/users/services/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/users/urls.py +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/setup.cfg +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/setup.py +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/tests/__init__.py +0 -0
- {bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/tests/test_email.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BmorricalDjangoWorkflows
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: Reusable Django Workflows
|
|
5
5
|
Home-page: https://github.com/Bmorrical/django-workflows
|
|
6
6
|
Author: Bradley Morrical
|
|
@@ -78,6 +78,7 @@ WORKFLOWS = {
|
|
|
78
78
|
# Optional settings with defaults shown.
|
|
79
79
|
"COMPANY_NAME": "My Company",
|
|
80
80
|
"ENABLE_EMAIL_DELIVERY": False,
|
|
81
|
+
"FORGOT_PASSWORD_URL": "https://app.example.com/forgot-password",
|
|
81
82
|
"RESET_CODE_TTL_MINUTES": 10,
|
|
82
83
|
"MAX_VERIFY_ATTEMPTS": 3,
|
|
83
84
|
}
|
|
@@ -86,8 +87,21 @@ WORKFLOWS = {
|
|
|
86
87
|
Notes:
|
|
87
88
|
|
|
88
89
|
- `USER_META_MODEL` must be a dotted import path such as `myapp.models.UserMeta`.
|
|
90
|
+
- `FORGOT_PASSWORD_URL` is used in the admin-created account email to link users into the host app's forgot-password flow.
|
|
91
|
+
- Host apps will usually source `FORGOT_PASSWORD_URL` from an environment variable in their own settings module.
|
|
89
92
|
- If `ENABLE_EMAIL_DELIVERY` is true, configure the email transport environment variables used by the mail service.
|
|
90
93
|
|
|
94
|
+
Example host-project wiring:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import os
|
|
98
|
+
|
|
99
|
+
WORKFLOWS = {
|
|
100
|
+
"USER_META_MODEL": "users.models_user_meta.UserMeta",
|
|
101
|
+
"FORGOT_PASSWORD_URL": os.getenv("FORGOT_PASSWORD_URL", ""),
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
91
105
|
## Email Delivery Environment Variables
|
|
92
106
|
|
|
93
107
|
When email delivery is enabled, set:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: BmorricalDjangoWorkflows
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: Reusable Django Workflows
|
|
5
5
|
Home-page: https://github.com/Bmorrical/django-workflows
|
|
6
6
|
Author: Bradley Morrical
|
|
@@ -78,6 +78,7 @@ WORKFLOWS = {
|
|
|
78
78
|
# Optional settings with defaults shown.
|
|
79
79
|
"COMPANY_NAME": "My Company",
|
|
80
80
|
"ENABLE_EMAIL_DELIVERY": False,
|
|
81
|
+
"FORGOT_PASSWORD_URL": "https://app.example.com/forgot-password",
|
|
81
82
|
"RESET_CODE_TTL_MINUTES": 10,
|
|
82
83
|
"MAX_VERIFY_ATTEMPTS": 3,
|
|
83
84
|
}
|
|
@@ -86,8 +87,21 @@ WORKFLOWS = {
|
|
|
86
87
|
Notes:
|
|
87
88
|
|
|
88
89
|
- `USER_META_MODEL` must be a dotted import path such as `myapp.models.UserMeta`.
|
|
90
|
+
- `FORGOT_PASSWORD_URL` is used in the admin-created account email to link users into the host app's forgot-password flow.
|
|
91
|
+
- Host apps will usually source `FORGOT_PASSWORD_URL` from an environment variable in their own settings module.
|
|
89
92
|
- If `ENABLE_EMAIL_DELIVERY` is true, configure the email transport environment variables used by the mail service.
|
|
90
93
|
|
|
94
|
+
Example host-project wiring:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import os
|
|
98
|
+
|
|
99
|
+
WORKFLOWS = {
|
|
100
|
+
"USER_META_MODEL": "users.models_user_meta.UserMeta",
|
|
101
|
+
"FORGOT_PASSWORD_URL": os.getenv("FORGOT_PASSWORD_URL", ""),
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
91
105
|
## Email Delivery Environment Variables
|
|
92
106
|
|
|
93
107
|
When email delivery is enabled, set:
|
|
@@ -54,6 +54,7 @@ WORKFLOWS = {
|
|
|
54
54
|
# Optional settings with defaults shown.
|
|
55
55
|
"COMPANY_NAME": "My Company",
|
|
56
56
|
"ENABLE_EMAIL_DELIVERY": False,
|
|
57
|
+
"FORGOT_PASSWORD_URL": "https://app.example.com/forgot-password",
|
|
57
58
|
"RESET_CODE_TTL_MINUTES": 10,
|
|
58
59
|
"MAX_VERIFY_ATTEMPTS": 3,
|
|
59
60
|
}
|
|
@@ -62,8 +63,21 @@ WORKFLOWS = {
|
|
|
62
63
|
Notes:
|
|
63
64
|
|
|
64
65
|
- `USER_META_MODEL` must be a dotted import path such as `myapp.models.UserMeta`.
|
|
66
|
+
- `FORGOT_PASSWORD_URL` is used in the admin-created account email to link users into the host app's forgot-password flow.
|
|
67
|
+
- Host apps will usually source `FORGOT_PASSWORD_URL` from an environment variable in their own settings module.
|
|
65
68
|
- If `ENABLE_EMAIL_DELIVERY` is true, configure the email transport environment variables used by the mail service.
|
|
66
69
|
|
|
70
|
+
Example host-project wiring:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import os
|
|
74
|
+
|
|
75
|
+
WORKFLOWS = {
|
|
76
|
+
"USER_META_MODEL": "users.models_user_meta.UserMeta",
|
|
77
|
+
"FORGOT_PASSWORD_URL": os.getenv("FORGOT_PASSWORD_URL", ""),
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
67
81
|
## Email Delivery Environment Variables
|
|
68
82
|
|
|
69
83
|
When email delivery is enabled, set:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.1.4
|
|
@@ -26,6 +26,7 @@ _ALLOWED_CODE_CHARS = [
|
|
|
26
26
|
]
|
|
27
27
|
_LOG_DEV_CODES_SETTING = "LOG_VERIFICATION_CODES_IN_DEBUG"
|
|
28
28
|
_EMAIL_DELIVERY_ENABLED_SETTING = "ENABLE_EMAIL_DELIVERY"
|
|
29
|
+
_FORGOT_PASSWORD_URL_SETTING = "FORGOT_PASSWORD_URL"
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def generate_verification_code(*, length: int = 6) -> str:
|
|
@@ -314,6 +315,32 @@ def _get_force_password_reset(user: Any) -> bool:
|
|
|
314
315
|
return False
|
|
315
316
|
|
|
316
317
|
|
|
318
|
+
def _build_admin_setup_email_message(
|
|
319
|
+
*,
|
|
320
|
+
first_name: str,
|
|
321
|
+
last_name: str,
|
|
322
|
+
company_name: str,
|
|
323
|
+
forgot_password_url: Any,
|
|
324
|
+
) -> str:
|
|
325
|
+
forgot_password_instructions = (
|
|
326
|
+
'Use the "Forgot Password" flow to establish your password and login.<br><br>'
|
|
327
|
+
)
|
|
328
|
+
normalized_forgot_password_url = str(forgot_password_url or "").strip()
|
|
329
|
+
|
|
330
|
+
if normalized_forgot_password_url:
|
|
331
|
+
forgot_password_instructions = (
|
|
332
|
+
f'Click the link <a href="{normalized_forgot_password_url}">{normalized_forgot_password_url}</a> to use the '
|
|
333
|
+
'"Forgot Password" flow to establish your password and login.<br><br>'
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
f"Hello {first_name} {last_name},<br><br>"
|
|
338
|
+
'You have been added as a registered user by an administrator. '
|
|
339
|
+
f"{forgot_password_instructions}"
|
|
340
|
+
f"Thank you!<br><br>{company_name}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
317
344
|
def get_user_details(user: Any) -> dict[str, Any]:
|
|
318
345
|
return {
|
|
319
346
|
"id": user.id,
|
|
@@ -505,6 +532,7 @@ def admin_register_user_and_send_setup_email(
|
|
|
505
532
|
User = get_user_model()
|
|
506
533
|
|
|
507
534
|
company_name = _get_workflow_setting("COMPANY_NAME", "")
|
|
535
|
+
forgot_password_url = _get_workflow_setting(_FORGOT_PASSWORD_URL_SETTING, "")
|
|
508
536
|
enable_email_delivery = _is_enabled(
|
|
509
537
|
_get_workflow_setting(_EMAIL_DELIVERY_ENABLED_SETTING, False)
|
|
510
538
|
)
|
|
@@ -537,11 +565,11 @@ def admin_register_user_and_send_setup_email(
|
|
|
537
565
|
)
|
|
538
566
|
_sync_user_groups(user=user, groups=validated_groups)
|
|
539
567
|
|
|
540
|
-
message = (
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
568
|
+
message = _build_admin_setup_email_message(
|
|
569
|
+
first_name=first_name,
|
|
570
|
+
last_name=last_name,
|
|
571
|
+
company_name=company_name,
|
|
572
|
+
forgot_password_url=forgot_password_url,
|
|
545
573
|
)
|
|
546
574
|
except IntegrityError:
|
|
547
575
|
logger.exception("Admin registration failed due to IntegrityError for email=%s", email)
|
|
@@ -609,11 +637,9 @@ def update_user(user: Any, data: dict[str, Any]) -> UserUpdateResult:
|
|
|
609
637
|
)
|
|
610
638
|
|
|
611
639
|
email_value = data.get("email")
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
user.username = next_email
|
|
616
|
-
user.email = next_email
|
|
640
|
+
if email_value is not None:
|
|
641
|
+
user.username = email_value
|
|
642
|
+
user.email = email_value
|
|
617
643
|
processed_update = True
|
|
618
644
|
user_fields_updated = True
|
|
619
645
|
|
{bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/users/views.py
RENAMED
|
@@ -276,8 +276,8 @@ class UserListView(APIView):
|
|
|
276
276
|
|
|
277
277
|
# Route: GET /user/
|
|
278
278
|
# Route: PATCH /user/
|
|
279
|
-
# PATCH body params: optional id,
|
|
280
|
-
# Notes: when id is omitted the authenticated user is updated; updating another user by id requires the User Manager group
|
|
279
|
+
# PATCH body params: optional id, email, firstName, lastName, password, isActive, optional groups as [{name, enabled}]
|
|
280
|
+
# Notes: when id is omitted the authenticated user is updated; updating another user by id requires the User Manager group; username stays aligned to email
|
|
281
281
|
class UserDetailView(APIView):
|
|
282
282
|
permission_classes = [IsAuthenticated]
|
|
283
283
|
|
|
@@ -47,6 +47,7 @@ class AuthFlowTests(TestCase):
|
|
|
47
47
|
defaults = {
|
|
48
48
|
"COMPANY_NAME": "Acme Co",
|
|
49
49
|
"ENABLE_EMAIL_DELIVERY": False,
|
|
50
|
+
"FORGOT_PASSWORD_URL": "",
|
|
50
51
|
"RESET_CODE_TTL_MINUTES": 10,
|
|
51
52
|
"MAX_VERIFY_ATTEMPTS": 3,
|
|
52
53
|
}
|
|
@@ -75,6 +76,35 @@ class AuthFlowTests(TestCase):
|
|
|
75
76
|
self.assertIsInstance(password, str)
|
|
76
77
|
self.assertTrue(password)
|
|
77
78
|
|
|
79
|
+
def test_build_admin_setup_email_message_includes_forgot_password_link_when_configured(self):
|
|
80
|
+
message = auth_flow._build_admin_setup_email_message(
|
|
81
|
+
first_name="New",
|
|
82
|
+
last_name="User",
|
|
83
|
+
company_name="Reusable Inc",
|
|
84
|
+
forgot_password_url="https://app.example.com/forgot-password",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self.assertIn(
|
|
88
|
+
'Click the link <a href="https://app.example.com/forgot-password">https://app.example.com/forgot-password</a> to use the '
|
|
89
|
+
'"Forgot Password" flow to establish your password and login.<br><br>',
|
|
90
|
+
message,
|
|
91
|
+
)
|
|
92
|
+
self.assertIn("Reusable Inc", message)
|
|
93
|
+
|
|
94
|
+
def test_build_admin_setup_email_message_falls_back_when_url_missing(self):
|
|
95
|
+
message = auth_flow._build_admin_setup_email_message(
|
|
96
|
+
first_name="New",
|
|
97
|
+
last_name="User",
|
|
98
|
+
company_name="Reusable Inc",
|
|
99
|
+
forgot_password_url=" ",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self.assertIn(
|
|
103
|
+
'Use the "Forgot Password" flow to establish your password and login.<br><br>',
|
|
104
|
+
message,
|
|
105
|
+
)
|
|
106
|
+
self.assertNotIn("href=", message)
|
|
107
|
+
|
|
78
108
|
@patch.object(auth_flow.logger, "isEnabledFor", return_value=True)
|
|
79
109
|
@patch.object(auth_flow.logger, "debug")
|
|
80
110
|
@patch.object(auth_flow, "_get_workflow_setting", return_value=True)
|
|
@@ -757,6 +787,7 @@ class AuthFlowTests(TestCase):
|
|
|
757
787
|
mock_get_setting.side_effect = self._settings_getter(
|
|
758
788
|
ENABLE_EMAIL_DELIVERY=True,
|
|
759
789
|
COMPANY_NAME="Reusable Inc",
|
|
790
|
+
FORGOT_PASSWORD_URL="https://app.example.com/forgot-password",
|
|
760
791
|
)
|
|
761
792
|
mock_send_email.return_value = SimpleNamespace(status_code=200, text="ok")
|
|
762
793
|
|
|
@@ -807,7 +838,7 @@ class AuthFlowTests(TestCase):
|
|
|
807
838
|
self.assertEqual(kwargs["to"], ["new@example.com"])
|
|
808
839
|
self.assertEqual(kwargs["subject"], "Your account has been created")
|
|
809
840
|
self.assertIn(
|
|
810
|
-
'You have been added as a registered user by an administrator.
|
|
841
|
+
'You have been added as a registered user by an administrator. Click the link <a href="https://app.example.com/forgot-password">https://app.example.com/forgot-password</a> to use the "Forgot Password" flow to establish your password and login.',
|
|
811
842
|
kwargs["html"],
|
|
812
843
|
)
|
|
813
844
|
self.assertIn("Reusable Inc", kwargs["html"])
|
|
@@ -1526,6 +1557,10 @@ class AuthFlowTests(TestCase):
|
|
|
1526
1557
|
self.assertTrue(result.success)
|
|
1527
1558
|
self.assertEqual(user.first_name, "Updated")
|
|
1528
1559
|
self.assertEqual(user.last_name, "Person")
|
|
1560
|
+
self.assertEqual(user.username, "member@example.com")
|
|
1561
|
+
self.assertEqual(user.email, "member@example.com")
|
|
1562
|
+
user.set_password.assert_not_called()
|
|
1563
|
+
user.save.assert_called_once()
|
|
1529
1564
|
mock_sync_user_groups.assert_not_called()
|
|
1530
1565
|
|
|
1531
1566
|
@patch.object(auth_flow, "_get_group_model")
|
|
@@ -1607,6 +1642,26 @@ class AuthFlowTests(TestCase):
|
|
|
1607
1642
|
self.assertEqual(result.message, "groups must be a list of objects with name and enabled.")
|
|
1608
1643
|
user.save.assert_not_called()
|
|
1609
1644
|
|
|
1645
|
+
def test_update_user_ignores_username_when_email_is_omitted(self):
|
|
1646
|
+
user = SimpleNamespace(
|
|
1647
|
+
username="member@example.com",
|
|
1648
|
+
email="member@example.com",
|
|
1649
|
+
first_name="Member",
|
|
1650
|
+
last_name="User",
|
|
1651
|
+
is_active=True,
|
|
1652
|
+
groups=SimpleNamespace(add=MagicMock(), remove=MagicMock()),
|
|
1653
|
+
save=MagicMock(),
|
|
1654
|
+
set_password=MagicMock(),
|
|
1655
|
+
)
|
|
1656
|
+
|
|
1657
|
+
result = auth_flow.update_user(user, {"username": "updated@example.com"})
|
|
1658
|
+
|
|
1659
|
+
self.assertFalse(result.success)
|
|
1660
|
+
self.assertEqual(result.message, "No valid fields provided.")
|
|
1661
|
+
self.assertEqual(user.username, "member@example.com")
|
|
1662
|
+
self.assertEqual(user.email, "member@example.com")
|
|
1663
|
+
user.save.assert_not_called()
|
|
1664
|
+
|
|
1610
1665
|
def test_update_user_returns_conflict_when_save_hits_integrity_error(self):
|
|
1611
1666
|
user = SimpleNamespace(
|
|
1612
1667
|
username="member@example.com",
|
|
@@ -1623,7 +1678,6 @@ class AuthFlowTests(TestCase):
|
|
|
1623
1678
|
user,
|
|
1624
1679
|
{
|
|
1625
1680
|
"email": "taken@example.com",
|
|
1626
|
-
"username": "taken@example.com",
|
|
1627
1681
|
},
|
|
1628
1682
|
)
|
|
1629
1683
|
|
|
@@ -373,7 +373,6 @@ class PackageUserDetailViewTest(SimpleTestCase):
|
|
|
373
373
|
data={
|
|
374
374
|
"id": 9,
|
|
375
375
|
"email": "target@example.com",
|
|
376
|
-
"username": "target@example.com",
|
|
377
376
|
"isActive": 1,
|
|
378
377
|
"groups": [{"name": "Time Entry User", "enabled": True}],
|
|
379
378
|
},
|
|
@@ -402,7 +401,6 @@ class PackageUserDetailViewTest(SimpleTestCase):
|
|
|
402
401
|
data={
|
|
403
402
|
"id": 9,
|
|
404
403
|
"email": "member@example.com",
|
|
405
|
-
"username": "member@example.com",
|
|
406
404
|
"firstName": "Updated",
|
|
407
405
|
},
|
|
408
406
|
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.1.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/services/email.py
RENAMED
|
File without changes
|
{bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/users/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{bmorricaldjangoworkflows-2.1.2 → bmorricaldjangoworkflows-2.1.4}/django_workflows/users/urls.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|