openwisp-users 1.3.0__tar.gz → 1.3.2__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 (135) hide show
  1. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/CHANGES.rst +22 -0
  2. {openwisp_users-1.3.0/openwisp_users.egg-info → openwisp_users-1.3.2}/PKG-INFO +1 -1
  3. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/__init__.py +1 -1
  4. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/static/openwisp-users/accounts/css/non-admin.css +5 -0
  5. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/base/models.py +2 -2
  6. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/static/openwisp-users/js/org-autocomplete.js +3 -0
  7. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tasks.py +5 -2
  8. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_models.py +94 -7
  9. {openwisp_users-1.3.0 → openwisp_users-1.3.2/openwisp_users.egg-info}/PKG-INFO +1 -1
  10. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/LICENSE +0 -0
  11. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/MANIFEST.in +0 -0
  12. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/README.rst +0 -0
  13. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/__init__.py +0 -0
  14. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/adapter.py +0 -0
  15. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/locale/de/LC_MESSAGES/django.po +0 -0
  16. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/locale/fur/LC_MESSAGES/django.po +0 -0
  17. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/locale/it/LC_MESSAGES/django.po +0 -0
  18. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/locale/ru/LC_MESSAGES/django.po +0 -0
  19. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/locale/sl/LC_MESSAGES/django.po +0 -0
  20. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/static/openwisp-users/accounts/css/change-password.css +0 -0
  21. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/account_expiration_reminder_message.html +0 -0
  22. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/account_expiration_reminder_message.txt +0 -0
  23. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/account_expired_message.html +0 -0
  24. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/account_expired_message.txt +0 -0
  25. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/base_message.txt +0 -0
  26. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/email_confirmation_message.html +0 -0
  27. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/email_confirmation_message.txt +0 -0
  28. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/email_confirmation_signup_message.html +0 -0
  29. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/password_expiration_message.html +0 -0
  30. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email/password_expiration_message.txt +0 -0
  31. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email_confirm.html +0 -0
  32. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/email_verification_success.html +0 -0
  33. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/login.html +0 -0
  34. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/logout_success.html +0 -0
  35. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/password_change.html +0 -0
  36. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/password_change_success.html +0 -0
  37. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/account/password_not_required.html +0 -0
  38. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/templates/allauth/layouts/base.html +0 -0
  39. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/urls.py +0 -0
  40. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/accounts/views.py +0 -0
  41. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/admin.py +0 -0
  42. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/__init__.py +0 -0
  43. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/authentication.py +0 -0
  44. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/filters.py +0 -0
  45. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/mixins.py +0 -0
  46. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/permissions.py +0 -0
  47. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/serializers.py +0 -0
  48. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/swagger.py +0 -0
  49. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/throttling.py +0 -0
  50. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/urls.py +0 -0
  51. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/api/views.py +0 -0
  52. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/apps.py +0 -0
  53. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/auth.py +0 -0
  54. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/backends.py +0 -0
  55. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/base/__init__.py +0 -0
  56. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/base/forms.py +0 -0
  57. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/locale/de/LC_MESSAGES/django.po +0 -0
  58. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/locale/fur/LC_MESSAGES/django.po +0 -0
  59. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/locale/it/LC_MESSAGES/django.po +0 -0
  60. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/locale/ru/LC_MESSAGES/django.po +0 -0
  61. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/locale/sl/LC_MESSAGES/django.po +0 -0
  62. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/management/__init__.py +0 -0
  63. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/management/commands/__init__.py +0 -0
  64. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/management/commands/export_users.py +0 -0
  65. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/middleware.py +0 -0
  66. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0001_initial.py +0 -0
  67. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0002_auto_20180508_2017.py +0 -0
  68. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0003_default_organization.py +0 -0
  69. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0004_default_groups.py +0 -0
  70. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0005_user_phone_number.py +0 -0
  71. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0006_id_email_index_together.py +0 -0
  72. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0007_unique_email.py +0 -0
  73. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0008_update_admins_permissions.py +0 -0
  74. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0009_create_organization_owners.py +0 -0
  75. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0010_allow_admins_change_organization.py +0 -0
  76. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0011_user_first_name_150_max_length.py +0 -0
  77. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0012_user_location_max_length_256.py +0 -0
  78. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0013_user_birth_date.py +0 -0
  79. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0014_user_notes.py +0 -0
  80. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0015_alter_organization_users_alter_organizationuser_user.py +0 -0
  81. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0016_organizationinvitation.py +0 -0
  82. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0017_user_language.py +0 -0
  83. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0018_allow_operator_view_organisation.py +0 -0
  84. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0019_user_password_updated.py +0 -0
  85. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0020_populate_password_updated_field.py +0 -0
  86. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0021_rename_user_id_email_openwisp_us_id_06c07a_idx.py +0 -0
  87. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0022_user_expiration_date.py +0 -0
  88. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0023_add_auth_token_permissions_to_admins.py +0 -0
  89. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0024_apikey.py +0 -0
  90. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0025_alter_organizationuser_is_admin.py +0 -0
  91. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/0026_user_password_based_token.py +0 -0
  92. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/migrations/__init__.py +0 -0
  93. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/mixins.py +0 -0
  94. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/models.py +0 -0
  95. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/multitenancy.py +0 -0
  96. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/password_validation.py +0 -0
  97. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/settings.py +0 -0
  98. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/static/openwisp-users/css/admin.css +0 -0
  99. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/static/openwisp-users/css/change-password.css +0 -0
  100. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/static/openwisp-users/js/addform.js +0 -0
  101. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/templates/admin/action_confirmation.html +0 -0
  102. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/templates/admin/login.html +0 -0
  103. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/templates/admin/openwisp_users/action_confirmation.html +0 -0
  104. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/templates/admin/openwisp_users/login.html +0 -0
  105. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/templates/admin/openwisp_users/user/change_form.html +0 -0
  106. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/templates/custom_password_reset_email.html +0 -0
  107. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/templates/custom_password_reset_email.txt +0 -0
  108. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/__init__.py +0 -0
  109. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_accounts.py +0 -0
  110. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_adapter.py +0 -0
  111. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_admin.py +0 -0
  112. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_api/__init__.py +0 -0
  113. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_api/test_api.py +0 -0
  114. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_api/test_authentication.py +0 -0
  115. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_api/test_password_reset.py +0 -0
  116. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_api/test_throttling.py +0 -0
  117. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_api/test_urls.py +0 -0
  118. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_api/test_views.py +0 -0
  119. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_auth.py +0 -0
  120. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_backends.py +0 -0
  121. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_commands.py +0 -0
  122. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/test_middlewares.py +0 -0
  123. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/tests/utils.py +0 -0
  124. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/utils.py +0 -0
  125. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/views.py +0 -0
  126. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users/widgets.py +0 -0
  127. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users.egg-info/SOURCES.txt +0 -0
  128. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users.egg-info/dependency_links.txt +0 -0
  129. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users.egg-info/not-zip-safe +0 -0
  130. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users.egg-info/requires.txt +0 -0
  131. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/openwisp_users.egg-info/top_level.txt +0 -0
  132. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/pyproject.toml +0 -0
  133. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/requirements.txt +0 -0
  134. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/setup.cfg +0 -0
  135. {openwisp_users-1.3.0 → openwisp_users-1.3.2}/setup.py +0 -0
@@ -1,6 +1,28 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ Version 1.3.2 [2026-10-23]
5
+ --------------------------
6
+
7
+ Bugfixes
8
+ ~~~~~~~~
9
+
10
+ - Restored logo padding on mobile screens
11
+
12
+ Rules in `openwisp.css` removed the logo padding on non-admin screens,
13
+ making it appear cramped.
14
+
15
+ - Corrected password expiration dates and duplicate notifications `#375
16
+ <https://github.com/openwisp/openwisp-users/issues/375>`_
17
+
18
+ Version 1.3.1 [2026-09-09]
19
+ --------------------------
20
+
21
+ Bugfixes
22
+ ~~~~~~~~
23
+
24
+ - Prevented autocomplete loop without an organization field
25
+
4
26
  Version 1.3.0 [2026-09-02]
5
27
  --------------------------
6
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwisp-users
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Provides basic multi-tenancy features for OpenWISP (using the Django web-framework)
5
5
  Home-page: http://openwisp.org
6
6
  Download-URL: https://github.com/openwisp/openwisp-users/releases
@@ -1,4 +1,4 @@
1
- VERSION = (1, 3, 0, "final")
1
+ VERSION = (1, 3, 2, "final")
2
2
  __version__ = VERSION # alias
3
3
 
4
4
 
@@ -5,3 +5,8 @@ button,
5
5
  form label {
6
6
  margin-right: 5px;
7
7
  }
8
+ @media (max-width: 768px) {
9
+ h1#site-name {
10
+ padding: 0.5rem 0;
11
+ }
12
+ }
@@ -129,7 +129,7 @@ class AbstractUser(BaseUser):
129
129
  return str(pk)
130
130
 
131
131
  def set_password(self, *args, **kwargs):
132
- self.password_updated = timezone.now().date()
132
+ self.password_updated = localdate()
133
133
  return super().set_password(*args, **kwargs)
134
134
 
135
135
  def has_password_expired(self):
@@ -145,7 +145,7 @@ class AbstractUser(BaseUser):
145
145
  )
146
146
  else:
147
147
  return False
148
- return expiry_date < timezone.now().date()
148
+ return expiry_date < localdate()
149
149
 
150
150
  def is_member(self, organization):
151
151
  return self._get_pk(organization) in self.organizations_dict
@@ -10,6 +10,9 @@
10
10
  // Hence, we need to update the value of selected option before
11
11
  // submission of form.
12
12
  var formElement = $("select#id_organization");
13
+ if (formElement.length === 0) {
14
+ return;
15
+ }
13
16
  while (formElement.prop("tagName") !== "FORM") {
14
17
  formElement = formElement.parent();
15
18
  }
@@ -6,7 +6,7 @@ from django.db.models import Q
6
6
  from django.template.loader import render_to_string
7
7
  from django.urls import reverse
8
8
  from django.utils import translation
9
- from django.utils.timezone import now, timedelta
9
+ from django.utils.timezone import localdate, timedelta
10
10
  from django.utils.translation import gettext_lazy as _
11
11
  from swapper import load_model
12
12
 
@@ -30,7 +30,7 @@ def password_expiration_email():
30
30
  ):
31
31
  # The password expiration feature is not enabled
32
32
  return
33
- expiry_date = now().date() + timedelta(days=7)
33
+ expiry_date = localdate() + timedelta(days=7)
34
34
  query = Q()
35
35
  if app_settings.USER_PASSWORD_EXPIRATION:
36
36
  query |= Q(
@@ -54,6 +54,9 @@ def password_expiration_email():
54
54
  emailaddress__verified=True,
55
55
  )
56
56
  .filter(query)
57
+ # a user can own more than one verified email address:
58
+ # without distinct() the join would send one email per address
59
+ .distinct()
57
60
  )
58
61
  email_count = 0
59
62
  for user in qs.iterator():
@@ -416,7 +416,7 @@ class TestUsers(TestOrganizationMixin, TestCase):
416
416
 
417
417
  # User.objects.create_user does not call User.set_password.
418
418
  # Therefore, we set the password_updated field manually.
419
- User.objects.update(password_updated=now().date())
419
+ User.objects.update(password_updated=localdate())
420
420
  staff_user.refresh_from_db()
421
421
  end_user.refresh_from_db()
422
422
 
@@ -437,7 +437,7 @@ class TestUsers(TestOrganizationMixin, TestCase):
437
437
  self.assertEqual(end_user.has_password_expired(), False)
438
438
 
439
439
  with self.subTest("Test password is expired"):
440
- User.objects.update(password_updated=now().date() - timedelta(days=180))
440
+ User.objects.update(password_updated=localdate() - timedelta(days=180))
441
441
  staff_user.refresh_from_db()
442
442
  end_user.refresh_from_db()
443
443
  with (
@@ -459,10 +459,10 @@ class TestUsers(TestOrganizationMixin, TestCase):
459
459
  @patch.object(app_settings, "USER_PASSWORD_EXPIRATION", 30)
460
460
  @patch.object(app_settings, "STAFF_USER_PASSWORD_EXPIRATION", 90)
461
461
  def test_password_expiration_mail(self):
462
- user_expiry_date = now().date() - timedelta(
462
+ user_expiry_date = localdate() - timedelta(
463
463
  days=(app_settings.USER_PASSWORD_EXPIRATION - 7)
464
464
  )
465
- staff_user_expiry_date = now().date() - timedelta(
465
+ staff_user_expiry_date = localdate() - timedelta(
466
466
  days=(app_settings.STAFF_USER_PASSWORD_EXPIRATION - 7)
467
467
  )
468
468
  staff_user = self._create_operator()
@@ -498,7 +498,7 @@ class TestUsers(TestOrganizationMixin, TestCase):
498
498
  password_updated=user_expiry_date
499
499
  )
500
500
  password_expiration_email.delay()
501
- expected_date = localize((now() + timedelta(days=7)).date())
501
+ expected_date = localize(localdate() + timedelta(days=7))
502
502
  self.assertEqual(len(mail.outbox), 1)
503
503
  email = mail.outbox.pop()
504
504
  self.assertEqual(email.to, [verified_email_user.email])
@@ -525,7 +525,7 @@ class TestUsers(TestOrganizationMixin, TestCase):
525
525
  @patch.object(app_settings, "USER_PASSWORD_EXPIRATION", 30)
526
526
  @patch("openwisp_users.utils.sleep")
527
527
  def test_password_expiration_mail_sleep(self, mocked_sleep):
528
- user_expiry_date = now().date() - timedelta(
528
+ user_expiry_date = localdate() - timedelta(
529
529
  days=(app_settings.USER_PASSWORD_EXPIRATION - 7)
530
530
  )
531
531
  for i in range(10):
@@ -547,7 +547,7 @@ class TestUsers(TestOrganizationMixin, TestCase):
547
547
  self.assertEqual(app_settings.USER_PASSWORD_EXPIRATION, 0)
548
548
  self.assertEqual(app_settings.STAFF_USER_PASSWORD_EXPIRATION, 0)
549
549
  self._create_user()
550
- User.objects.update(password_updated=now().date() - timedelta(days=180))
550
+ User.objects.update(password_updated=localdate() - timedelta(days=180))
551
551
  with patch("openwisp_users.tasks.send_email") as mocked_send_email:
552
552
  password_expiration_email.delay()
553
553
  mocked_send_email.assert_not_called()
@@ -1195,3 +1195,90 @@ class TestUsers(TestOrganizationMixin, TestCase):
1195
1195
  expiration_reminder_email,
1196
1196
  None,
1197
1197
  )
1198
+
1199
+ @patch.object(app_settings, "USER_PASSWORD_EXPIRATION", 30)
1200
+ def test_password_expiration_mail_multiple_verified_emails(self):
1201
+ user = self._create_user()
1202
+ for index in range(2):
1203
+ EmailAddress.objects.create(
1204
+ user=user,
1205
+ email=f"alias{index}@tester.com",
1206
+ verified=True,
1207
+ )
1208
+ User.objects.update(
1209
+ password_updated=localdate()
1210
+ - timedelta(days=app_settings.USER_PASSWORD_EXPIRATION - 7)
1211
+ )
1212
+ password_expiration_email.delay()
1213
+ self.assertEqual(
1214
+ len(mail.outbox),
1215
+ 1,
1216
+ msg=(
1217
+ f"Expected 1 password expiry notice, got {len(mail.outbox)}:"
1218
+ " the notice is duplicated once per verified email address"
1219
+ " of the same user"
1220
+ ),
1221
+ )
1222
+ self.assertEqual(mail.outbox[0].to, [user.email])
1223
+
1224
+ @freeze_time("2026-03-29 23:30:00")
1225
+ @override_settings(TIME_ZONE="Europe/Rome")
1226
+ @patch.object(app_settings, "USER_PASSWORD_EXPIRATION", 30)
1227
+ def test_password_expiration_local_date(self):
1228
+ """
1229
+ 23:30 UTC is already the next day in Europe/Rome (UTC+2), so password
1230
+ expiration dates must follow the local calendar day.
1231
+ """
1232
+ user = self._create_user()
1233
+
1234
+ with self.subTest("password_updated is stamped with the local date"):
1235
+ user.set_password("tester")
1236
+ self.assertEqual(
1237
+ user.password_updated,
1238
+ localdate(),
1239
+ msg=(
1240
+ f"Expected password_updated {localdate()} (local date),"
1241
+ f" got {user.password_updated} (UTC date)"
1242
+ ),
1243
+ )
1244
+
1245
+ expiration = app_settings.USER_PASSWORD_EXPIRATION
1246
+
1247
+ with self.subTest("password is valid on the expiration date"):
1248
+ user.password_updated = localdate() - timedelta(days=expiration)
1249
+ self.assertFalse(
1250
+ user.has_password_expired(),
1251
+ msg="Expected password to remain valid on its expiration date",
1252
+ )
1253
+
1254
+ with self.subTest("password is expired after the expiration date"):
1255
+ user.password_updated = localdate() - timedelta(days=expiration + 1)
1256
+ self.assertTrue(
1257
+ user.has_password_expired(),
1258
+ msg=(
1259
+ "Expected the password to be expired: it expired on"
1260
+ f" {user.password_updated + timedelta(days=expiration)},"
1261
+ f" while the local date is {localdate()}"
1262
+ ),
1263
+ )
1264
+
1265
+ @freeze_time("2026-03-29 23:30:00")
1266
+ @override_settings(TIME_ZONE="Europe/Rome")
1267
+ @patch.object(app_settings, "USER_PASSWORD_EXPIRATION", 30)
1268
+ def test_password_expiration_mail_local_date(self):
1269
+ self._create_user()
1270
+ User.objects.update(
1271
+ password_updated=localdate()
1272
+ - timedelta(days=app_settings.USER_PASSWORD_EXPIRATION - 7)
1273
+ )
1274
+ password_expiration_email.delay()
1275
+ self.assertEqual(
1276
+ len(mail.outbox),
1277
+ 1,
1278
+ msg=(
1279
+ f"Expected 1 password expiry notice, got {len(mail.outbox)}:"
1280
+ " the task looks for passwords expiring 7 days after the UTC"
1281
+ f" date ({now().date()}) instead of the local date"
1282
+ f" ({localdate()})"
1283
+ ),
1284
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openwisp-users
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: Provides basic multi-tenancy features for OpenWISP (using the Django web-framework)
5
5
  Home-page: http://openwisp.org
6
6
  Download-URL: https://github.com/openwisp/openwisp-users/releases
File without changes
File without changes
File without changes