Skip to content

Commit c77567b

Browse files
authored
[deps] Added support for Django >=5.1,<5.3 and Python >=3.12, 3.14 #432
- Dropped support for Python < 3.9 - Dropped support for Django < 4.2 Closes #432
1 parent 8b27115 commit c77567b

File tree

9 files changed

+94
-22
lines changed

9 files changed

+94
-22
lines changed

.github/workflows/ci.yml

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,48 @@ jobs:
2424
fail-fast: false
2525
matrix:
2626
python-version:
27-
- "3.8"
2827
- "3.9"
2928
- "3.10"
29+
- "3.11"
30+
- "3.12"
31+
- "3.13"
3032
django-version:
31-
- django~=3.2.0
32-
- django~=4.1.0
3333
- django~=4.2.0
34+
- django~=5.0.0
35+
- django~=5.1.0
36+
- django~=5.2rc0
37+
exclude:
38+
# Django 5.0+ requires Python >=3.10
39+
- python-version: "3.9"
40+
django-version: django~=5.0.0
41+
- python-version: "3.9"
42+
django-version: django~=5.1.0
43+
- python-version: "3.9"
44+
django-version: django~=5.2rc0
45+
# Python 3.13 supported only in Django >=5.1.3
46+
- python-version: "3.13"
47+
django-version: django~=4.2.0
48+
- python-version: "3.13"
49+
django-version: django~=5.0.0
3450

3551
steps:
3652
- uses: actions/checkout@v4
3753
with:
3854
ref: ${{ github.event.pull_request.head.sha }}
3955

56+
- name: Cache APT packages
57+
uses: actions/cache@v4
58+
with:
59+
path: /var/cache/apt/archives
60+
key: apt-${{ runner.os }}-${{ hashFiles('.github/workflows/ci.yml') }}
61+
restore-keys: |
62+
apt-${{ runner.os }}-
63+
64+
- name: Disable man page auto-update
65+
run: |
66+
echo 'set man-db/auto-update false' | sudo debconf-communicate >/dev/null
67+
sudo dpkg-reconfigure man-db
68+
4069
- name: Set up Python ${{ matrix.python-version }}
4170
uses: actions/setup-python@v5
4271
with:
@@ -50,7 +79,7 @@ jobs:
5079
run: |
5180
sudo apt update -qq
5281
sudo apt-get -qq -y install gettext
53-
pip install -U pip wheel
82+
pip install -U pip wheel setuptools
5483
pip install -U -r requirements-test.txt
5584
sudo npm install -g prettier
5685
pip install -e .[rest]
@@ -69,6 +98,13 @@ jobs:
6998
NO_SOCIAL_APP=1 coverage run ./tests/manage.py test testapp.tests.test_admin.TestUsersAdmin --parallel
7099
coverage combine
71100
coverage xml
101+
env:
102+
SELENIUM_HEADLESS: 1
103+
GECKO_LOG: 1
104+
105+
- name: Show gecko web driver log on failures
106+
if: ${{ failure() }}
107+
run: cat geckodriver.log
72108

73109
- name: Upload Coverage
74110
if: ${{ success() }}

openwisp_users/base/models.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class AbstractUser(BaseUser):
7373

7474
class Meta(BaseUser.Meta):
7575
abstract = True
76-
index_together = ('id', 'email')
76+
indexes = [models.Index(fields=['id', 'email'], name='user_id_email_idx')]
7777

7878
@staticmethod
7979
def _get_pk(obj):
@@ -257,7 +257,11 @@ class Meta:
257257
abstract = True
258258

259259
def clean(self):
260-
if self.user.is_owner(self.organization_id) and not self.is_admin:
260+
if (
261+
not self._state.adding
262+
and self.user.is_owner(self.organization_id)
263+
and not self.is_admin
264+
):
261265
raise ValidationError(
262266
_(
263267
f'{self.user.username} is the owner of the organization: '
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
# Generated by Django 2.1.7 on 2019-04-24 11:41
22

3-
from django.db import migrations
3+
from django.db import migrations, models
44

55

66
class Migration(migrations.Migration):
77
dependencies = [('openwisp_users', '0005_user_phone_number')]
88

99
operations = [
10-
migrations.AlterIndexTogether(name='user', index_together={('id', 'email')})
10+
migrations.AddIndex(
11+
model_name='user',
12+
index=models.Index(
13+
fields=['id', 'email'],
14+
name='user_id_email_idx',
15+
),
16+
),
1117
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 4.2.20 on 2025-03-27 07:14
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
('openwisp_users', '0020_populate_password_updated_field'),
9+
]
10+
11+
operations = [
12+
migrations.RenameIndex(
13+
model_name='user',
14+
new_name='user_id_email_idx',
15+
old_fields=('id', 'email'),
16+
),
17+
]

openwisp_users/tests/test_admin.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import uuid
55
from unittest.mock import patch
66

7+
import django
78
from django.contrib import admin
89
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
910
from django.contrib.auth.models import Permission
@@ -252,9 +253,11 @@ def test_admin_change_user_reuse_password(self):
252253
self.assertContains(
253254
response,
254255
(
255-
'<ul class="errorlist"><li>'
256+
'<ul class="errorlist"{}><li>'
256257
'You cannot re-use your current password. '
257258
'Enter a new password.</li></ul>'
259+
).format(
260+
' id="id_password2_error"' if django.VERSION >= (5, 2) else ''
258261
),
259262
)
260263
with override_settings(AUTH_PASSWORD_VALIDATORS=[]):
@@ -344,7 +347,7 @@ def test_admin_change_non_superuser_readonly_fields(self):
344347
with self.subTest('User Permissions'):
345348
# regex to check if `<div class="readonly"> ... app_label </div>`
346349
# exists in the response
347-
html = f'<div class="readonly">((?!</div>).)*({self.app_label})'
350+
html = f'v((?!</div>).)*({self.app_label})'
348351
self.assertTrue(
349352
re.search(
350353
html,

openwisp_users/tests/test_api/test_api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ def test_post_email_list_api(self):
440440
self.assertEqual(EmailAddress.objects.filter(user=user1).count(), 1)
441441
path = reverse('users:email_list', args=(user1.pk,))
442442
data = {'email': '[email protected]'}
443-
expected_queries = 7 if django.VERSION < (4, 0) else 9
443+
expected_queries = 9 if django.VERSION < (5, 2) else 13
444444
with self.assertNumQueries(expected_queries):
445445
response = self.client.post(path, data, content_type='application/json')
446446
self.assertEqual(response.status_code, 201)
@@ -468,7 +468,7 @@ def test_put_email_update_api(self):
468468
email_id = EmailAddress.objects.get(user=user1).id
469469
path = reverse('users:email_update', args=(user1.pk, email_id))
470470
data = {'email': '[email protected]', 'primary': True}
471-
expected_queries = 9 if django.VERSION < (4, 0) else 11
471+
expected_queries = 11 if django.VERSION < (5, 2) else 15
472472
with self.assertNumQueries(expected_queries):
473473
response = self.client.put(path, data, content_type='application/json')
474474
self.assertEqual(response.status_code, 200)
@@ -479,7 +479,7 @@ def test_patch_email_update_api(self):
479479
email_id = EmailAddress.objects.get(user=user1).id
480480
path = reverse('users:email_update', args=(user1.pk, email_id))
481481
data = {'email': '[email protected]'}
482-
expected_queries = 9 if django.VERSION < (4, 0) else 11
482+
expected_queries = 11 if django.VERSION < (5, 2) else 15
483483
with self.assertNumQueries(expected_queries):
484484
response = self.client.patch(path, data, content_type='application/json')
485485
self.assertEqual(response.status_code, 200)

tests/openwisp2/sample_users/migrations/0001_initial.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,12 @@ class Migration(migrations.Migration):
199199
'verbose_name': 'user',
200200
'verbose_name_plural': 'users',
201201
'abstract': False,
202-
'index_together': {('id', 'email')},
202+
'indexes': [
203+
models.Index(
204+
fields=['id', 'email'],
205+
name='user_id_email_idx',
206+
)
207+
],
203208
},
204209
managers=[
205210
('objects', openwisp_users.base.models.UserManager()),

tests/testapp/tests/test_admin.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22

3+
import django
34
from django.contrib.auth import get_user_model
45
from django.test import TestCase
56
from django.urls import reverse
@@ -60,9 +61,9 @@ def test_org_admin_create_shareable_template(self):
6061
response,
6162
(
6263
'<div class="form-row errors field-organization">\n'
63-
' <ul class="errorlist"><li>This field '
64-
'is required.</li></ul>'
65-
),
64+
' <ul class="errorlist"{}>'
65+
'<li>This field is required.</li></ul>'
66+
).format(' id="id_organization_error"' if django.VERSION >= (5, 2) else ''),
6667
)
6768
self.assertEqual(Template.objects.count(), 0)
6869

tests/testapp/tests/test_selenium.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def test_book_add_form_organization_field(self):
6464
visible=Organization.objects.values_list('name', flat=True),
6565
hidden=[],
6666
)
67-
self.open(reverse('admin:logout'))
67+
self.logout()
6868

6969
with self.subTest('Test organization user: 1 org'):
7070
self._test_multitenant_autocomplete_org_field(
@@ -81,7 +81,7 @@ def test_book_add_form_organization_field(self):
8181
)
8282
self.assertEqual(len(org_select.all_selected_options), 1)
8383
self.assertEqual(org_select.first_selected_option.text, org1.name)
84-
self.open(reverse('admin:logout'))
84+
self.logout()
8585

8686
with self.subTest('Test organization user: 2 orgs'):
8787
self._create_org_user(user=administrator, organization=org2, is_admin=True)
@@ -99,7 +99,7 @@ def test_book_add_form_organization_field(self):
9999
self.web_driver.find_element(By.CSS_SELECTOR, '#id_organization')
100100
)
101101
self.assertEqual(len(org_select.all_selected_options), 0)
102-
self.open(reverse('admin:logout'))
102+
self.logout()
103103

104104
def test_shelf_add_form_organization_field(self):
105105
path = reverse('admin:testapp_shelf_add')
@@ -122,7 +122,7 @@ def test_shelf_add_form_organization_field(self):
122122
+ ['Shared systemwide (no organization)'],
123123
hidden=[],
124124
)
125-
self.open(reverse('admin:logout'))
125+
self.logout()
126126

127127
with self.subTest('Test organization user'):
128128
self._test_multitenant_autocomplete_org_field(
@@ -142,4 +142,4 @@ def test_shelf_add_form_organization_field(self):
142142
)
143143
self.assertEqual(len(org_select.all_selected_options), 1)
144144
self.assertEqual(org_select.first_selected_option.text, org1.name)
145-
self.open(reverse('admin:logout'))
145+
self.logout()

0 commit comments

Comments
 (0)