Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,48 @@ jobs:
fail-fast: false
matrix:
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
django-version:
- django~=3.2.0
- django~=4.1.0
- django~=4.2.0
- django~=5.0.0
- django~=5.1.0
- django~=5.2rc0
exclude:
# Django 5.0+ requires Python >=3.10
- python-version: "3.9"
django-version: django~=5.0.0
- python-version: "3.9"
django-version: django~=5.1.0
- python-version: "3.9"
django-version: django~=5.2rc0
# Python 3.13 supported only in Django >=5.1.3
- python-version: "3.13"
django-version: django~=4.2.0
- python-version: "3.13"
django-version: django~=5.0.0

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

- name: Cache APT packages
uses: actions/cache@v4
with:
path: /var/cache/apt/archives
key: apt-${{ runner.os }}-${{ hashFiles('.github/workflows/ci.yml') }}
restore-keys: |
apt-${{ runner.os }}-

- name: Disable man page auto-update
run: |
echo 'set man-db/auto-update false' | sudo debconf-communicate >/dev/null
sudo dpkg-reconfigure man-db

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
Expand All @@ -50,10 +79,11 @@ jobs:
run: |
sudo apt update -qq
sudo apt-get -qq -y install gettext
pip install -U pip wheel
pip install -U pip wheel setuptools
pip install -U -r requirements-test.txt
sudo npm install -g prettier
pip install -e .[rest]
pip install -IU "openwisp-utils[qa,selenium] @ https://github.com/openwisp/openwisp-utils/tarball/issues/439-deps"
Comment thread
nemesifier marked this conversation as resolved.
Outdated
pip install -U ${{ matrix.django-version }}

- name: QA checks
Expand All @@ -69,6 +99,13 @@ jobs:
NO_SOCIAL_APP=1 coverage run ./tests/manage.py test testapp.tests.test_admin.TestUsersAdmin --parallel
coverage combine
coverage xml
env:
SELENIUM_HEADLESS: 1
GECKO_LOG: 1

- name: Show gecko web driver log on failures
if: ${{ failure() }}
run: cat geckodriver.log

- name: Upload Coverage
if: ${{ success() }}
Expand Down
8 changes: 6 additions & 2 deletions openwisp_users/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class AbstractUser(BaseUser):

class Meta(BaseUser.Meta):
abstract = True
index_together = ('id', 'email')
indexes = [models.Index(fields=['id', 'email'], name='user_id_email_idx')]

@staticmethod
def _get_pk(obj):
Expand Down Expand Up @@ -257,7 +257,11 @@ class Meta:
abstract = True

def clean(self):
if self.user.is_owner(self.organization_id) and not self.is_admin:
if (
not self._state.adding
and self.user.is_owner(self.organization_id)
and not self.is_admin
):
raise ValidationError(
_(
f'{self.user.username} is the owner of the organization: '
Expand Down
10 changes: 8 additions & 2 deletions openwisp_users/migrations/0006_id_email_index_together.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# Generated by Django 2.1.7 on 2019-04-24 11:41

from django.db import migrations
from django.db import migrations, models


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

operations = [
migrations.AlterIndexTogether(name='user', index_together={('id', 'email')})
migrations.AddIndex(
model_name='user',
index=models.Index(
fields=['id', 'email'],
name='user_id_email_idx',
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.20 on 2025-03-27 07:14

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
('openwisp_users', '0020_populate_password_updated_field'),
]

operations = [
migrations.RenameIndex(
model_name='user',
new_name='user_id_email_idx',
old_fields=('id', 'email'),
),
]
7 changes: 5 additions & 2 deletions openwisp_users/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import uuid
from unittest.mock import patch

import django
from django.contrib import admin
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
from django.contrib.auth.models import Permission
Expand Down Expand Up @@ -252,9 +253,11 @@ def test_admin_change_user_reuse_password(self):
self.assertContains(
response,
(
'<ul class="errorlist"><li>'
'<ul class="errorlist"{}><li>'
'You cannot re-use your current password. '
'Enter a new password.</li></ul>'
).format(
' id="id_password2_error"' if django.VERSION >= (5, 2) else ''
),
)
with override_settings(AUTH_PASSWORD_VALIDATORS=[]):
Expand Down Expand Up @@ -344,7 +347,7 @@ def test_admin_change_non_superuser_readonly_fields(self):
with self.subTest('User Permissions'):
# regex to check if `<div class="readonly"> ... app_label </div>`
# exists in the response
html = f'<div class="readonly">((?!</div>).)*({self.app_label})'
html = f'v((?!</div>).)*({self.app_label})'
self.assertTrue(
re.search(
html,
Expand Down
6 changes: 3 additions & 3 deletions openwisp_users/tests/test_api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ def test_post_email_list_api(self):
self.assertEqual(EmailAddress.objects.filter(user=user1).count(), 1)
path = reverse('users:email_list', args=(user1.pk,))
data = {'email': 'newemail@test.com'}
expected_queries = 7 if django.VERSION < (4, 0) else 9
expected_queries = 9 if django.VERSION < (5, 2) else 13
with self.assertNumQueries(expected_queries):
response = self.client.post(path, data, content_type='application/json')
self.assertEqual(response.status_code, 201)
Expand Down Expand Up @@ -468,7 +468,7 @@ def test_put_email_update_api(self):
email_id = EmailAddress.objects.get(user=user1).id
path = reverse('users:email_update', args=(user1.pk, email_id))
data = {'email': 'emailchange@test.com', 'primary': True}
expected_queries = 9 if django.VERSION < (4, 0) else 11
expected_queries = 11 if django.VERSION < (5, 2) else 15
with self.assertNumQueries(expected_queries):
response = self.client.put(path, data, content_type='application/json')
self.assertEqual(response.status_code, 200)
Expand All @@ -479,7 +479,7 @@ def test_patch_email_update_api(self):
email_id = EmailAddress.objects.get(user=user1).id
path = reverse('users:email_update', args=(user1.pk, email_id))
data = {'email': 'changemail@test.com'}
expected_queries = 9 if django.VERSION < (4, 0) else 11
expected_queries = 11 if django.VERSION < (5, 2) else 15
with self.assertNumQueries(expected_queries):
response = self.client.patch(path, data, content_type='application/json')
self.assertEqual(response.status_code, 200)
Expand Down
7 changes: 6 additions & 1 deletion tests/openwisp2/sample_users/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,12 @@ class Migration(migrations.Migration):
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
'index_together': {('id', 'email')},
'indexes': [
models.Index(
fields=['id', 'email'],
name='user_id_email_idx',
)
],
},
managers=[
('objects', openwisp_users.base.models.UserManager()),
Expand Down
7 changes: 4 additions & 3 deletions tests/testapp/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os

import django
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
Expand Down Expand Up @@ -60,9 +61,9 @@ def test_org_admin_create_shareable_template(self):
response,
(
'<div class="form-row errors field-organization">\n'
' <ul class="errorlist"><li>This field '
'is required.</li></ul>'
),
' <ul class="errorlist"{}>'
'<li>This field is required.</li></ul>'
).format(' id="id_organization_error"' if django.VERSION >= (5, 2) else ''),
)
self.assertEqual(Template.objects.count(), 0)

Expand Down
10 changes: 5 additions & 5 deletions tests/testapp/tests/test_selenium.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_book_add_form_organization_field(self):
visible=Organization.objects.values_list('name', flat=True),
hidden=[],
)
self.open(reverse('admin:logout'))
self.logout()

with self.subTest('Test organization user: 1 org'):
self._test_multitenant_autocomplete_org_field(
Expand All @@ -81,7 +81,7 @@ def test_book_add_form_organization_field(self):
)
self.assertEqual(len(org_select.all_selected_options), 1)
self.assertEqual(org_select.first_selected_option.text, org1.name)
self.open(reverse('admin:logout'))
self.logout()

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

def test_shelf_add_form_organization_field(self):
path = reverse('admin:testapp_shelf_add')
Expand All @@ -122,7 +122,7 @@ def test_shelf_add_form_organization_field(self):
+ ['Shared systemwide (no organization)'],
hidden=[],
)
self.open(reverse('admin:logout'))
self.logout()

with self.subTest('Test organization user'):
self._test_multitenant_autocomplete_org_field(
Expand All @@ -142,4 +142,4 @@ def test_shelf_add_form_organization_field(self):
)
self.assertEqual(len(org_select.all_selected_options), 1)
self.assertEqual(org_select.first_selected_option.text, org1.name)
self.open(reverse('admin:logout'))
self.logout()
Loading