Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
19 changes: 15 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,30 @@ on:
jobs:
build:
name: Python==${{ matrix.python-version }} | ${{ matrix.django-version }}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04

strategy:
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.1.0
- django~=5.2.0
exclude:
# Django 5.1+ requires Python >=3.10
- python-version: "3.9"
django-version: django~=5.1.0
- python-version: "3.9"
django-version: django~=5.2.0
# Python 3.13 supported only in Django >=5.1.3
- python-version: "3.13"
django-version: django~=4.2.0

steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 1 addition & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# NOTE: This Docker image is for development purposes only.

version: "3"

services:
controller:
image: openwisp/controller-development:latest
Expand All @@ -21,7 +19,7 @@ services:
entrypoint: redis-server --appendonly yes

postgres:
image: postgis/postgis:13-3.3-alpine
image: postgis/postgis:17-3.5-alpine
environment:
POSTGRES_PASSWORD: openwisp2
POSTGRES_USER: openwisp2
Expand Down
2 changes: 1 addition & 1 deletion docs/developer/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Developer Installation Instructions
Dependencies
------------

- Python >= 3.8
- Python >= 3.9
- OpenSSL

Installing for Development
Expand Down
11 changes: 3 additions & 8 deletions openwisp_controller/config/base/vpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from copy import deepcopy
from subprocess import CalledProcessError, TimeoutExpired

import django
import shortuuid
from cache_memoize import cache_memoize
from django.core.cache import cache
Expand Down Expand Up @@ -856,13 +855,9 @@ def register_auto_ip_stopper(cls, func):
cls._auto_ip_stopper_funcs.append(func)

def _get_unique_checks(self, exclude=None, include_meta_constraints=False):
if django.VERSION < (4, 1):
# TODO: Remove when dropping support for Django 3.2
unique_checks, date_checks = super()._get_unique_checks(exclude)
else:
unique_checks, date_checks = super()._get_unique_checks(
exclude, include_meta_constraints
)
unique_checks, date_checks = super()._get_unique_checks(
exclude, include_meta_constraints
)

if not self.vpn._vxlan_vni:
# If VNI is not specified in VXLAN tunnel configuration,
Expand Down
2 changes: 1 addition & 1 deletion openwisp_controller/config/tests/test_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -1346,7 +1346,7 @@ def test_ip_fields_not_duplicated(self):
self.assertIsNone(c1.device.management_ip)
self.assertEqual(c2.device.management_ip, '192.168.1.99')
# other organization is not affected
self.assertEquals(c3.device.last_ip, '127.0.0.1')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is removed in Python 3.12

self.assertEqual(c3.device.last_ip, '127.0.0.1')
self.assertEqual(c3.device.management_ip, '192.168.1.99')

with self.subTest('test interaction with DeviceChecksumView caching'):
Expand Down
9 changes: 5 additions & 4 deletions openwisp_controller/config/tests/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from openwisp_utils.tests import catch_signal

from ...tests.utils import TransactionTestMixin
from .. import settings as app_settings
from ..signals import config_modified, config_status_changed
from ..tasks import logger as task_logger
Expand Down Expand Up @@ -356,7 +355,10 @@ def test_context_regression(self):
template_qs = Template.objects.filter(type='vpn')
self.assertEqual(template_qs.count(), 1)
t = template_qs.first()
self.assertDictContainsSubset(_original_context, t.get_context())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is removed in Python 3.12

context = t.get_context()
# check all items from original context exist in template context
for key, value in _original_context.items():
self.assertEqual(context.get(key), value)
self.assertEqual(app_settings.CONTEXT, _original_context)

with self.subTest(
Expand Down Expand Up @@ -517,7 +519,6 @@ def test_regression_preventing_from_fixing_invalid_conf(self):


class TestTemplateTransaction(
TransactionTestMixin,
CreateConfigTemplateMixin,
TestVpnX509Mixin,
TransactionTestCase,
Expand Down Expand Up @@ -554,7 +555,7 @@ def test_config_status_modified_after_change(self):
with catch_signal(config_status_changed) as handler:
t.config['interfaces'][0]['name'] = 'eth2'
t.full_clean()
with self.assertNumQueries(9):
with self.assertNumQueries(10):
t.save()
c.refresh_from_db()
handler.assert_not_called()
Expand Down
15 changes: 14 additions & 1 deletion openwisp_controller/connection/base/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import collections
import logging

import django
import jsonschema
from django.core.exceptions import ValidationError
from django.db import models, transaction
Expand All @@ -24,6 +25,7 @@
ORGANIZATION_COMMAND_SCHEMA,
ORGANIZATION_ENABLED_COMMANDS,
get_command_callable,
get_command_choices,
get_command_schema,
)
from ..exceptions import NoWorkingDeviceConnectionError
Expand Down Expand Up @@ -408,7 +410,18 @@ class AbstractCommand(TimeStampedEditableModel):
status = models.CharField(
max_length=12, choices=STATUS_CHOICES, default=STATUS_CHOICES[0][0]
)
type = models.CharField(max_length=16, choices=COMMAND_CHOICES)
type = models.CharField(
max_length=16,
choices=(
COMMAND_CHOICES
if django.VERSION < (5, 0)
# In Django 5.0+, choices are normalized at model definition,
# creating a static list of tuples that doesn't update when command
# are dynamically registered or unregistered. Using a callable
# ensures we always get the current choices from the registry.
else get_command_choices
),
)
input = JSONField(
blank=True,
null=True,
Expand Down
7 changes: 7 additions & 0 deletions openwisp_controller/connection/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,10 @@ def _unregister_command_choice(command):
ORGANIZATION_COMMAND_SCHEMA[org_id] = OrderedDict()
for command in commands:
ORGANIZATION_COMMAND_SCHEMA[org_id][command] = COMMANDS[command]['schema']


def get_command_choices():
"""
Returns the command choices.
"""
return COMMAND_CHOICES
7 changes: 5 additions & 2 deletions openwisp_controller/connection/migrations/0007_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import collections
import uuid

import django
import django.db.migrations.operations.special
import django.db.models.deletion
import django.utils.timezone
Expand All @@ -11,7 +12,7 @@
import swapper
from django.db import migrations, models

from ..commands import COMMAND_CHOICES
from ..commands import COMMAND_CHOICES, get_command_choices
from . import assign_command_permissions_to_groups


Expand Down Expand Up @@ -65,7 +66,9 @@ class Migration(migrations.Migration):
(
'type',
models.CharField(
choices=COMMAND_CHOICES,
choices=COMMAND_CHOICES
if django.VERSION < (5, 0)
else get_command_choices,
max_length=16,
),
),
Expand Down
7 changes: 3 additions & 4 deletions openwisp_controller/connection/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

from openwisp_utils.tests import capture_any_output, catch_signal

from ...tests.utils import TransactionTestMixin
from .. import settings as app_settings
from ..commands import (
COMMANDS,
Expand Down Expand Up @@ -888,7 +887,7 @@ def test_command_multiple_connections(self, connect_mocked):
self.assertIn(command.connection, [dc1, dc2])


class TestModelsTransaction(TransactionTestMixin, BaseTestModels, TransactionTestCase):
class TestModelsTransaction(BaseTestModels, TransactionTestCase):
def _prepare_conf_object(self, organization=None):
if not organization:
organization = self._create_org(name='org1')
Expand Down Expand Up @@ -1121,12 +1120,12 @@ def test_chunk_size(self):
organization=org, name='device3', mac_address='33:33:33:33:33:33'
)
)
with self.assertNumQueries(31):
with self.assertNumQueries(32):
credential = self._create_credentials(auto_add=True, organization=org)
self.assertEqual(credential.deviceconnection_set.count(), 3)

with mock.patch.object(Credentials, 'chunk_size', 2):
with self.assertNumQueries(33):
with self.assertNumQueries(35):
credential = self._create_credentials(
name='Mocked Credential', auto_add=True, organization=org
)
6 changes: 2 additions & 4 deletions openwisp_controller/connection/tests/test_ssh.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
import sys
from unittest import mock

from django.conf import settings
Expand Down Expand Up @@ -54,9 +53,8 @@ def test_connection_connect_auth_failure(self, mocked_ssh_close):
self.assertEqual(mocked_connect.call_count, 2)
self.assertFalse(dc.is_working)
self.assertEqual(mocked_ssh_close.call_count, 2)
if sys.version_info[0:2] > (3, 7):
self.assertNotIn('disabled_algorithms', mocked_connect.mock_calls[0].kwargs)
self.assertIn('disabled_algorithms', mocked_connect.mock_calls[1].kwargs)
self.assertNotIn('disabled_algorithms', mocked_connect.mock_calls[0].kwargs)
self.assertIn('disabled_algorithms', mocked_connect.mock_calls[1].kwargs)

@mock.patch.object(ssh_logger, 'info')
@mock.patch.object(ssh_logger, 'debug')
Expand Down
2 changes: 1 addition & 1 deletion openwisp_controller/connection/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@


def get_connection_working_notification_target_url(obj, field, absolute_url=True):
url = _get_object_link(obj, field, absolute_url)
url = _get_object_link(obj._related_object(field), absolute_url)
return f'{url}#deviceconnection_set-group'
1 change: 1 addition & 0 deletions openwisp_controller/geo/tests/test_admin_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,4 @@ def test_add_mobile(self):


del TestConfigAdmin
del BaseTestAdminInline
2 changes: 1 addition & 1 deletion openwisp_controller/geo/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ def test_change_location_type_to_outdoor_api(self):
self._create_floorplan(location=l1)
path = reverse('geo_api:detail_location', args=[l1.pk])
data = {'type': 'outdoor'}
with self.assertNumQueries(8):
with self.assertNumQueries(9):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number of queries increased because of these changes openwisp/django-loci#157

response = self.client.patch(path, data, content_type='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['floorplan'], [])
Expand Down
33 changes: 0 additions & 33 deletions openwisp_controller/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import django
from django.contrib.auth import get_user_model
from django.db import connections
from django.db.utils import DEFAULT_DB_ALIAS
from django.test.testcases import _AssertNumQueriesContext
from django.urls import reverse

from openwisp_users.tests.utils import TestMultitenantAdminMixin
Expand All @@ -20,32 +16,3 @@ def _test_changelist_recover_deleted(self, app_label, model_label):

def _login(self, username='admin', password='tester'):
self.client.force_login(user_model.objects.get(username=username))


class _ManagementTransactionNumQueriesContext(_AssertNumQueriesContext):
def __exit__(self, exc_type, exc_value, traceback):
"""
Django 4.2 introduced support for logging transaction
management queries (BEGIN, COMMIT, and ROLLBACK).
This method increases the number of expected database
queries if COMMIT/ROLLBACK queries are found when
using Django 4.2
"""
if exc_type is not None:
return
for query in self.captured_queries:
if django.VERSION > (4, 2) and 'COMMIT' in query['sql']:
self.num += 1
super().__exit__(exc_type, exc_value, traceback)


class TransactionTestMixin(object):
def assertNumQueries(self, num, func=None, *args, using=DEFAULT_DB_ALIAS, **kwargs):
conn = connections[using]

context = _ManagementTransactionNumQueriesContext(self, num, conn)
if func is None:
return context

with context:
func(*args, **kwargs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks!

5 changes: 1 addition & 4 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
pytest-django~=4.9.0
pytest-asyncio~=0.24.0
pytest-cov~=5.0.0
openwisp-utils[qa,selenium] @ https://github.com/openwisp/openwisp-utils/tarball/1.2
channels_redis~=4.2.1
openwisp-utils[qa,selenium,channels-test] @ https://github.com/openwisp/openwisp-utils/tarball/1.2
django_redis~=5.4.0
mock-ssh-server~=0.9.1
responses~=0.25.6
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
django-sortedm2m~=4.0.0
django-reversion~=5.1.0
django-taggit~=4.0.0
django-taggit~=6.0.0
netjsonconfig @ https://github.com/openwisp/netjsonconfig/tarball/1.2
django-x509 @ https://github.com/openwisp/django-x509/tarball/1.3
django-loci @ https://github.com/openwisp/django-loci/tarball/1.2
django-flat-json-widget~=0.3.1
openwisp-users @ https://github.com/openwisp/openwisp-users/tarball/1.2
openwisp-utils[celery] @ https://github.com/openwisp/openwisp-utils/tarball/1.2
openwisp-utils[celery,channels] @ https://github.com/openwisp/openwisp-utils/tarball/1.2
openwisp-notifications @ https://github.com/openwisp/openwisp-notifications/tarball/1.2
openwisp-ipam @ https://github.com/openwisp/openwisp-ipam/tarball/1.2
djangorestframework-gis @ https://github.com/openwisp/django-rest-framework-gis/tarball/1.2
Expand Down
4 changes: 3 additions & 1 deletion tests/openwisp2/asgi.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application

from openwisp_controller.routing import get_routes

application = ProtocolTypeRouter(
{
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(get_routes()))
)
),
'http': get_asgi_application(),
}
)
7 changes: 5 additions & 2 deletions tests/openwisp2/sample_connection/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import collections
import uuid

import django
import django.db.models.deletion
import django.utils.timezone
import jsonfield.fields
Expand All @@ -14,7 +15,7 @@
import openwisp_controller.connection.base.models
import openwisp_users.mixins
from openwisp_controller.connection import settings as connection_settings
from openwisp_controller.connection.commands import COMMAND_CHOICES
from openwisp_controller.connection.commands import COMMAND_CHOICES, get_command_choices


class Migration(migrations.Migration):
Expand Down Expand Up @@ -245,7 +246,9 @@ class Migration(migrations.Migration):
(
'type',
models.CharField(
choices=COMMAND_CHOICES,
choices=COMMAND_CHOICES
if django.VERSION < (5, 0)
else get_command_choices,
max_length=16,
),
),
Expand Down
Loading
Loading