diff --git a/openwisp_controller/config/admin.py b/openwisp_controller/config/admin.py index bae451912..8ebc5192f 100644 --- a/openwisp_controller/config/admin.py +++ b/openwisp_controller/config/admin.py @@ -15,6 +15,7 @@ ObjectDoesNotExist, ValidationError, ) +from django.db import models from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse from django.http.response import HttpResponseForbidden from django.shortcuts import get_object_or_404 @@ -217,6 +218,9 @@ def _get_preview_instance(self, request): key = "{relation}_id".format(relation=key) # pass non-empty string or None kwargs[key] = value or None + # parse JSON strings for JSONField fields + elif isinstance(field, models.JSONField) and isinstance(value, str): + kwargs[key] = json.loads(value) if value else None # put regular field values in kwargs dict else: kwargs[key] = value diff --git a/openwisp_controller/config/base/base.py b/openwisp_controller/config/base/base.py index 8b55ad019..a3f579388 100644 --- a/openwisp_controller/config/base/base.py +++ b/openwisp_controller/config/base/base.py @@ -1,4 +1,3 @@ -import collections import hashlib import json import logging @@ -6,11 +5,12 @@ from cache_memoize import cache_memoize from django.core.exceptions import ValidationError +from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.db.models import JSONField from django.utils.functional import cached_property from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ -from jsonfield import JSONField from netjsonconfig.exceptions import ValidationError as SchemaError from openwisp_utils.base import TimeStampedEditableModel @@ -126,8 +126,7 @@ class BaseConfig(BaseModel): _("configuration"), default=dict, help_text=_("configuration in NetJSON DeviceConfiguration format"), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - dump_kwargs={"indent": 4}, + encoder=DjangoJSONEncoder, ) __template__ = False diff --git a/openwisp_controller/config/base/config.py b/openwisp_controller/config/base/config.py index efb42ff65..855d94541 100644 --- a/openwisp_controller/config/base/config.py +++ b/openwisp_controller/config/base/config.py @@ -6,9 +6,10 @@ from cache_memoize import cache_memoize from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist, PermissionDenied, ValidationError +from django.core.serializers.json import DjangoJSONEncoder from django.db import models, transaction +from django.db.models import JSONField from django.utils.translation import gettext_lazy as _ -from jsonfield import JSONField from model_utils import Choices from model_utils.fields import StatusField from netjsonconfig import OpenWrt @@ -90,8 +91,7 @@ class AbstractConfig(ChecksumCacheMixin, BaseConfig): '" target="_blank">' "configuration variables" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - dump_kwargs={"indent": 4}, + encoder=DjangoJSONEncoder, ) checksum_db = models.CharField( _("configuration checksum"), diff --git a/openwisp_controller/config/base/device_group.py b/openwisp_controller/config/base/device_group.py index 366d61667..d7f6be31a 100644 --- a/openwisp_controller/config/base/device_group.py +++ b/openwisp_controller/config/base/device_group.py @@ -1,11 +1,11 @@ -import collections from copy import deepcopy import jsonschema from django.core.exceptions import ValidationError +from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.db.models import JSONField from django.utils.translation import gettext_lazy as _ -from jsonfield import JSONField from jsonschema.exceptions import ValidationError as SchemaError from swapper import get_model_name, load_model @@ -39,24 +39,22 @@ class AbstractDeviceGroup(OrgMixin, TimeStampedEditableModel): meta_data = JSONField( blank=True, default=dict, - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - dump_kwargs={"indent": 4}, help_text=_( "Store custom metadata related to this group. This field is intended " "for arbitrary data that does not affect device configuration and can " "be retrieved via the REST API for integrations or external tools." ), verbose_name=_("Metadata"), + encoder=DjangoJSONEncoder, ) context = JSONField( blank=True, default=dict, - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - dump_kwargs={"indent": 4}, help_text=_( "Define configuration variables available to all devices in this group" ), verbose_name=_("Configuration Variables"), + encoder=DjangoJSONEncoder, ) def __str__(self): diff --git a/openwisp_controller/config/base/multitenancy.py b/openwisp_controller/config/base/multitenancy.py index 07c34bb68..67b96761e 100644 --- a/openwisp_controller/config/base/multitenancy.py +++ b/openwisp_controller/config/base/multitenancy.py @@ -1,11 +1,11 @@ -import collections from copy import deepcopy import swapper from django.core.exceptions import ValidationError +from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.db.models import JSONField from django.utils.translation import gettext_lazy as _ -from jsonfield import JSONField from openwisp_utils.base import KeyField, UUIDModel from openwisp_utils.fields import FallbackBooleanChoiceField @@ -42,13 +42,12 @@ class AbstractOrganizationConfigSettings(UUIDModel): context = JSONField( blank=True, default=dict, - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - dump_kwargs={"indent": 4}, help_text=_( "Define reusable configuration variables available " "to all devices in this organization" ), verbose_name=_("Configuration Variables"), + encoder=DjangoJSONEncoder, ) class Meta: diff --git a/openwisp_controller/config/base/template.py b/openwisp_controller/config/base/template.py index 25e89eee3..555366b7b 100644 --- a/openwisp_controller/config/base/template.py +++ b/openwisp_controller/config/base/template.py @@ -4,9 +4,10 @@ from copy import copy from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.serializers.json import DjangoJSONEncoder from django.db import models, transaction +from django.db.models import JSONField from django.utils.translation import gettext_lazy as _ -from jsonfield import JSONField from netjsonconfig.exceptions import ValidationError as NetjsonconfigValidationError from swapper import get_model_name, load_model from taggit.managers import TaggableManager @@ -101,8 +102,7 @@ class AbstractTemplate(ShareableOrgMixinUniqueName, BaseConfig): "These values are used during validation and when a variable is " "not provided by the device, group, or organization." ), - load_kwargs={"object_pairs_hook": OrderedDict}, - dump_kwargs={"indent": 4}, + encoder=DjangoJSONEncoder, ) __template__ = True diff --git a/openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py b/openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py index 274fe2d44..0121967ee 100644 --- a/openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py +++ b/openwisp_controller/config/migrations/0001_squashed_0002_config_settings_uuid.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # Generated by Django 1.10.5 on 2017-05-03 17:54 -import collections import re import uuid import django.core.validators import django.utils.timezone -import jsonfield.fields import model_utils.fields from django.conf import settings from django.db import migrations, models @@ -58,12 +56,10 @@ class Migration(migrations.Migration): ), ( "config", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text="configuration in NetJSON DeviceConfiguration format", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="configuration", ), ), @@ -250,12 +246,10 @@ class Migration(migrations.Migration): ), ( "config", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text="configuration in NetJSON DeviceConfiguration format", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="configuration", ), ), @@ -347,11 +341,9 @@ class Migration(migrations.Migration): ("name", models.CharField(max_length=64, unique=True)), ( "config", - jsonfield.fields.JSONField( + models.JSONField( default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text="configuration in NetJSON DeviceConfiguration format", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="configuration", ), ), diff --git a/openwisp_controller/config/migrations/0018_config_context.py b/openwisp_controller/config/migrations/0018_config_context.py index 5b897cde9..9aa0e5156 100644 --- a/openwisp_controller/config/migrations/0018_config_context.py +++ b/openwisp_controller/config/migrations/0018_config_context.py @@ -1,9 +1,7 @@ # Generated by Django 2.1.4 on 2019-01-09 01:52 -import collections -import jsonfield.fields -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -13,15 +11,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name="config", name="context", - field=jsonfield.fields.JSONField( + field=models.JSONField( blank=True, - dump_kwargs={"indent": 4}, help_text=( 'Additional context ' "(configuration variables) in JSON format" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, null=True, ), ) diff --git a/openwisp_controller/config/migrations/0023_update_context.py b/openwisp_controller/config/migrations/0023_update_context.py index 68edd30da..49c4cb850 100644 --- a/openwisp_controller/config/migrations/0023_update_context.py +++ b/openwisp_controller/config/migrations/0023_update_context.py @@ -1,9 +1,7 @@ # Generated by Django 3.0.3 on 2020-02-26 19:58 -import collections -import jsonfield.fields -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -13,17 +11,15 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="config", name="context", - field=jsonfield.fields.JSONField( + field=models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text=( "allows overriding " '' "configuration variables" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, ), ) ] diff --git a/openwisp_controller/config/migrations/0028_template_default_values.py b/openwisp_controller/config/migrations/0028_template_default_values.py index 13651f4dd..f67cca27e 100644 --- a/openwisp_controller/config/migrations/0028_template_default_values.py +++ b/openwisp_controller/config/migrations/0028_template_default_values.py @@ -1,9 +1,7 @@ # Generated by Django 3.0.3 on 2020-06-29 23:54 -import collections -import jsonfield.fields -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -13,16 +11,14 @@ class Migration(migrations.Migration): migrations.AddField( model_name="template", name="default_values", - field=jsonfield.fields.JSONField( + field=models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text=( "Define default values for the variables used in this template. " "These values are used during validation and when a variable is " "not provided by the device, group, or organization." ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="Default Values", ), ) diff --git a/openwisp_controller/config/migrations/0036_device_group.py b/openwisp_controller/config/migrations/0036_device_group.py index f3ca39ced..af180d116 100644 --- a/openwisp_controller/config/migrations/0036_device_group.py +++ b/openwisp_controller/config/migrations/0036_device_group.py @@ -1,11 +1,9 @@ # Generated by Django 3.1.12 on 2021-06-14 17:51 -import collections import uuid import django.db.models.deletion import django.utils.timezone -import jsonfield.fields import model_utils.fields import swapper from django.db import migrations, models @@ -56,11 +54,9 @@ class Migration(migrations.Migration): ), ( "meta_data", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, - load_kwargs={"object_pairs_hook": collections.OrderedDict}, help_text=( "Store custom metadata related to this group. " "This field is intended for arbitrary data that " diff --git a/openwisp_controller/config/migrations/0049_devicegroup_context.py b/openwisp_controller/config/migrations/0049_devicegroup_context.py index 34733f4db..a085c908c 100644 --- a/openwisp_controller/config/migrations/0049_devicegroup_context.py +++ b/openwisp_controller/config/migrations/0049_devicegroup_context.py @@ -1,9 +1,7 @@ # Generated by Django 3.2.19 on 2023-06-27 14:45 -import collections -import jsonfield.fields -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -15,15 +13,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name="devicegroup", name="context", - field=jsonfield.fields.JSONField( + field=models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text=( "Define configuration variables available " "to all devices in this group" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="Configuration Variables", ), ), diff --git a/openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py b/openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py index b643e2e12..e6de53839 100644 --- a/openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py +++ b/openwisp_controller/config/migrations/0051_organizationconfigsettings_context.py @@ -1,9 +1,7 @@ # Generated by Django 3.2.20 on 2023-07-22 10:49 -import collections -import jsonfield.fields -from django.db import migrations +from django.db import migrations, models class Migration(migrations.Migration): @@ -15,15 +13,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name="organizationconfigsettings", name="context", - field=jsonfield.fields.JSONField( + field=models.JSONField( blank=True, default=dict, - dump_kwargs={"indent": 4}, help_text=( "Define reusable configuration variables available " "to all devices in this organization" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="Configuration Variables", ), ), diff --git a/openwisp_controller/config/migrations/0064_replace_jsonfield_with_django_builtin.py b/openwisp_controller/config/migrations/0064_replace_jsonfield_with_django_builtin.py new file mode 100644 index 000000000..d985a5af3 --- /dev/null +++ b/openwisp_controller/config/migrations/0064_replace_jsonfield_with_django_builtin.py @@ -0,0 +1,125 @@ +# Generated by Django 5.2.12 on 2026-04-02 06:56 + +import django.core.serializers.json +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "config", + "0063_organizationconfigsettings_estimated_location_enabled_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="config", + name="config", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text="configuration in NetJSON DeviceConfiguration format", + verbose_name="configuration", + ), + ), + migrations.AlterField( + model_name="config", + name="context", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "allows overriding " + '' + "configuration variables" + ), + ), + ), + migrations.AlterField( + model_name="devicegroup", + name="context", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "Define configuration variables available " + "to all devices in this group" + ), + verbose_name="Configuration Variables", + ), + ), + migrations.AlterField( + model_name="devicegroup", + name="meta_data", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "Store custom metadata related to this group. " + "This field is intended for arbitrary data that " + "does not affect device configuration and can be " + "retrieved via the REST API for integrations or " + "external tools." + ), + verbose_name="Metadata", + ), + ), + migrations.AlterField( + model_name="organizationconfigsettings", + name="context", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "Define reusable configuration variables " + "available to all devices in this organization" + ), + verbose_name="Configuration Variables", + ), + ), + migrations.AlterField( + model_name="template", + name="config", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text="configuration in NetJSON DeviceConfiguration format", + verbose_name="configuration", + ), + ), + migrations.AlterField( + model_name="template", + name="default_values", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "Define default values for the variables used " + "in this template. These values are used during " + "validation and when a variable is not provided " + "by the device, group, or organization." + ), + verbose_name="Default Values", + ), + ), + migrations.AlterField( + model_name="vpn", + name="config", + field=models.JSONField( + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text="configuration in NetJSON DeviceConfiguration format", + verbose_name="configuration", + ), + ), + ] diff --git a/openwisp_controller/config/tests/test_api.py b/openwisp_controller/config/tests/test_api.py index 516b67fc3..6fcf9c527 100644 --- a/openwisp_controller/config/tests/test_api.py +++ b/openwisp_controller/config/tests/test_api.py @@ -428,8 +428,8 @@ def test_device_put_api(self): "backend": "netjsonconfig.OpenWisp", "status": "modified", "templates": [], - "context": "{}", - "config": "{}", + "context": {}, + "config": {}, }, } r = self.client.put(path, data, content_type="application/json") diff --git a/openwisp_controller/connection/base/models.py b/openwisp_controller/connection/base/models.py index 15f4eff63..441bfe73f 100644 --- a/openwisp_controller/connection/base/models.py +++ b/openwisp_controller/connection/base/models.py @@ -1,16 +1,16 @@ -import collections import logging import django import jsonschema from django.core.exceptions import ValidationError +from django.core.serializers.json import DjangoJSONEncoder from django.db import models, transaction +from django.db.models import JSONField from django.utils import timezone from django.utils.functional import cached_property from django.utils.module_loading import import_string from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ -from jsonfield import JSONField from jsonschema.exceptions import ValidationError as SchemaError from swapper import get_model_name, load_model @@ -101,8 +101,7 @@ class AbstractCredentials(ConnectorMixin, ShareableOrgMixinUniqueName, BaseModel _("parameters"), default=dict, help_text=_("global connection parameters"), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - dump_kwargs={"indent": 4}, + encoder=DjangoJSONEncoder, ) auto_add = models.BooleanField( _("auto add"), @@ -245,8 +244,7 @@ class AbstractDeviceConnection(ConnectorMixin, TimeStampedEditableModel): "local connection parameters (will override " "the global parameters if specified)" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - dump_kwargs={"indent": 4}, + encoder=DjangoJSONEncoder, ) # usability improvements is_working = models.BooleanField(null=True, blank=True, default=None) @@ -425,8 +423,7 @@ class AbstractCommand(TimeStampedEditableModel): input = JSONField( blank=True, null=True, - load_kwargs={"object_pairs_hook": collections.OrderedDict}, - dump_kwargs={"indent": 4}, + encoder=DjangoJSONEncoder, ) output = models.TextField(blank=True) diff --git a/openwisp_controller/connection/migrations/0001_initial.py b/openwisp_controller/connection/migrations/0001_initial.py index 24e902931..0e5553733 100644 --- a/openwisp_controller/connection/migrations/0001_initial.py +++ b/openwisp_controller/connection/migrations/0001_initial.py @@ -1,11 +1,8 @@ # Generated by Django 2.0.5 on 2018-05-05 17:33 -import collections import uuid import django.db.models.deletion -import django.utils.timezone -import jsonfield.fields import model_utils.fields import swapper from django.conf import settings @@ -70,11 +67,9 @@ class Migration(migrations.Migration): ), ( "params", - jsonfield.fields.JSONField( + models.JSONField( default=dict, - dump_kwargs={"indent": 4}, help_text="global connection parameters", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="parameters", ), ), @@ -137,15 +132,13 @@ class Migration(migrations.Migration): ("enabled", models.BooleanField(db_index=True, default=True)), ( "params", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"indent": 4}, help_text=( "local connection parameters (will override " "the global parameters if specified)" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="parameters", ), ), diff --git a/openwisp_controller/connection/migrations/0007_command.py b/openwisp_controller/connection/migrations/0007_command.py index ec04d55a1..5ec72cfac 100644 --- a/openwisp_controller/connection/migrations/0007_command.py +++ b/openwisp_controller/connection/migrations/0007_command.py @@ -1,13 +1,11 @@ # Generated by Django 3.1.2 on 2020-10-09 22:01 -import collections import uuid import django import django.db.migrations.operations.special import django.db.models.deletion import django.utils.timezone -import jsonfield.fields import model_utils.fields import swapper from django.db import migrations, models @@ -76,10 +74,8 @@ class Migration(migrations.Migration): ), ( "input", - jsonfield.fields.JSONField( + models.JSONField( blank=True, - dump_kwargs={"indent": 4}, - load_kwargs={"object_pairs_hook": collections.OrderedDict}, null=True, ), ), diff --git a/openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py b/openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py new file mode 100644 index 000000000..d83e0364a --- /dev/null +++ b/openwisp_controller/connection/migrations/0010_replace_jsonfield_with_django_builtin.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.11 on 2026-02-06 06:08 + +import django.core.serializers.json +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("connection", "0009_alter_deviceconnection_unique_together"), + ] + + operations = [ + migrations.AlterField( + model_name="command", + name="input", + field=models.JSONField( + blank=True, + encoder=django.core.serializers.json.DjangoJSONEncoder, + null=True, + ), + ), + migrations.AlterField( + model_name="credentials", + name="params", + field=models.JSONField( + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text="global connection parameters", + verbose_name="parameters", + ), + ), + migrations.AlterField( + model_name="deviceconnection", + name="params", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "local connection parameters (will override the global " + "parameters if specified)" + ), + verbose_name="parameters", + ), + ), + ] diff --git a/openwisp_controller/connection/tests/test_models.py b/openwisp_controller/connection/tests/test_models.py index 4766a3f7e..3dac7ffc6 100644 --- a/openwisp_controller/connection/tests/test_models.py +++ b/openwisp_controller/connection/tests/test_models.py @@ -502,7 +502,7 @@ def test_command_validation(self): with self.subTest("test extra arg on reboot"): command.type = "reboot" - command.input = '["test"]' + command.input = ["test"] with self.assertRaises(ValidationError) as context_manager: command.full_clean() e = context_manager.exception @@ -536,12 +536,12 @@ def test_command_validation(self): self.assertIn("input", e.message_dict) self.assertEqual( e.message_dict["input"], - ["Enter valid JSON.", "'notjson' is not of type 'object'"], + ["'notjson' is not of type 'object'"], ) with self.subTest("JSON check on arguments"): command.type = "change_password" - command.input = "[]" + command.input = [] with self.assertRaises(ValidationError) as context_manager: command.full_clean() e = context_manager.exception @@ -601,7 +601,7 @@ def test_arguments(self): self.assertEqual(list(command.arguments), ["newpwd", "newpwd"]) with self.subTest("value error"): - command = Command(input='["echo test"]', type="custom") + command = Command(input=["echo test"], type="custom") with self.assertRaises(TypeError) as context_manager: command.arguments self.assertEqual( @@ -972,7 +972,7 @@ def _assert_applying_conf_test_command(mocked_exec): self.assertEqual(conf.status, "modified") with self.subTest("openwisp_config >= 0.6.0a"): - conf.config = '{"dns_servers": []}' + conf.config = {"dns_servers": []} conf.full_clean() with mock.patch(_exec_command_path) as mocked_exec_command: mocked_exec_command.return_value = self._exec_command_return_value( @@ -987,7 +987,7 @@ def _assert_applying_conf_test_command(mocked_exec): self.assertEqual(conf.status, "modified") with self.subTest("openwisp_config < 0.6.0a: exit_code 0"): - conf.config = '{"interfaces": [{"name": "eth00","type": "ethernet"}]}' + conf.config = {"interfaces": [{"name": "eth00", "type": "ethernet"}]} conf.full_clean() with mock.patch(_exec_command_path) as mocked_exec_command: mocked_exec_command.return_value = self._exec_command_return_value( @@ -1001,7 +1001,7 @@ def _assert_applying_conf_test_command(mocked_exec): self.assertEqual(conf.status, "modified") with self.subTest("openwisp_config < 0.6.0a: exit_code 1"): - conf.config = '{"radios": []}' + conf.config = {"radios": []} conf.full_clean() with mock.patch(_exec_command_path) as mocked_exec_command: stdin, stdout, stderr = self._exec_command_return_value( diff --git a/openwisp_controller/pki/migrations/0001_initial.py b/openwisp_controller/pki/migrations/0001_initial.py index 014d050f8..5ac454a1b 100644 --- a/openwisp_controller/pki/migrations/0001_initial.py +++ b/openwisp_controller/pki/migrations/0001_initial.py @@ -1,12 +1,10 @@ # -*- coding: utf-8 -*- # Generated by Django 1.10.5 on 2017-03-07 17:49 -import collections import django.db.models.deletion import django.utils.timezone import django_x509.base.models -import jsonfield.fields import model_utils.fields from django.conf import settings from django.db import migrations, models @@ -111,12 +109,10 @@ class Migration(migrations.Migration): ), ( "extensions", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=list, - dump_kwargs={"indent": 4}, help_text="additional x509 certificate extensions", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="extensions", ), ), @@ -264,12 +260,10 @@ class Migration(migrations.Migration): ), ( "extensions", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=list, - dump_kwargs={"indent": 4}, help_text="additional x509 certificate extensions", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="extensions", ), ), diff --git a/requirements.txt b/requirements.txt index 386f4b9a2..770920c71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,5 +16,4 @@ django-cache-memoize~=0.2.1 shortuuid~=1.0.13 netaddr~=1.3.0 django-import-export~=4.3.14 -jsonfield>=3.1.0,<4.0.0 geoip2>=5.1.0,<6.0.0 diff --git a/tests/openwisp2/sample_config/migrations/0001_initial.py b/tests/openwisp2/sample_config/migrations/0001_initial.py index c6bb22c89..3944b083e 100644 --- a/tests/openwisp2/sample_config/migrations/0001_initial.py +++ b/tests/openwisp2/sample_config/migrations/0001_initial.py @@ -1,12 +1,10 @@ # Generated by Django 3.0.7 on 2020-06-27 11:16 -import collections import re import uuid import django.core.validators import django.db.models.deletion import django.utils.timezone -import jsonfield.fields import model_utils.fields import swapper import taggit.managers @@ -75,12 +73,10 @@ class Migration(migrations.Migration): ), ( "config", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text="configuration in NetJSON DeviceConfiguration format", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="configuration", ), ), @@ -116,17 +112,15 @@ class Migration(migrations.Migration): ), ( "context", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text=( "allows overriding " '' "configuration variables" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, ), ), ("details", models.CharField(blank=True, max_length=64, null=True)), @@ -244,11 +238,9 @@ class Migration(migrations.Migration): ("name", models.CharField(db_index=True, max_length=64, unique=True)), ( "config", - jsonfield.fields.JSONField( + models.JSONField( default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text="configuration in NetJSON DeviceConfiguration format", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="configuration", ), ), @@ -510,12 +502,10 @@ class Migration(migrations.Migration): ), ( "config", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text="configuration in NetJSON DeviceConfiguration format", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="configuration", ), ), @@ -575,17 +565,15 @@ class Migration(migrations.Migration): ), ( "default_values", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text=( "Define default values for the variables used " "in this template. These values are used during " "validation and when a variable is not provided " "by the device, group, or organization." ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="Default Values", ), ), @@ -706,15 +694,13 @@ class Migration(migrations.Migration): ), ( "context", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"indent": 4}, help_text=( "Define reusable configuration variables " "available to all devices in this organization" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="Configuration Variables", ), ), @@ -761,11 +747,9 @@ class Migration(migrations.Migration): ), ( "meta_data", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, - load_kwargs={"object_pairs_hook": collections.OrderedDict}, help_text=( "Store custom metadata related to this group. " "This field is intended for arbitrary data that " @@ -778,15 +762,13 @@ class Migration(migrations.Migration): ), ( "context", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"ensure_ascii": False, "indent": 4}, help_text=( "Define configuration variables available " "to all devices in this group" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="Configuration Variables", ), ), diff --git a/tests/openwisp2/sample_config/migrations/0010_replace_jsonfield_with_django_builtin.py b/tests/openwisp2/sample_config/migrations/0010_replace_jsonfield_with_django_builtin.py new file mode 100644 index 000000000..dfe029cf8 --- /dev/null +++ b/tests/openwisp2/sample_config/migrations/0010_replace_jsonfield_with_django_builtin.py @@ -0,0 +1,125 @@ +# Generated by Django 5.2.12 on 2026-04-02 07:00 + +import django.core.serializers.json +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "sample_config", + "0009_organizationconfigsettings_approximate_location_enabled_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="config", + name="config", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text="configuration in NetJSON DeviceConfiguration format", + verbose_name="configuration", + ), + ), + migrations.AlterField( + model_name="config", + name="context", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "allows overriding " + '' + "configuration variables" + ), + ), + ), + migrations.AlterField( + model_name="devicegroup", + name="context", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "Define configuration variables available " + "to all devices in this group" + ), + verbose_name="Configuration Variables", + ), + ), + migrations.AlterField( + model_name="devicegroup", + name="meta_data", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "Store custom metadata related to this group. " + "This field is intended for arbitrary data that " + "does not affect device configuration and can be " + "retrieved via the REST API for integrations or " + "external tools." + ), + verbose_name="Metadata", + ), + ), + migrations.AlterField( + model_name="organizationconfigsettings", + name="context", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "Define reusable configuration variables " + "available to all devices in this organization" + ), + verbose_name="Configuration Variables", + ), + ), + migrations.AlterField( + model_name="template", + name="config", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text="configuration in NetJSON DeviceConfiguration format", + verbose_name="configuration", + ), + ), + migrations.AlterField( + model_name="template", + name="default_values", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "Define default values for the variables used " + "in this template. These values are used during " + "validation and when a variable is not provided " + "by the device, group, or organization." + ), + verbose_name="Default Values", + ), + ), + migrations.AlterField( + model_name="vpn", + name="config", + field=models.JSONField( + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text="configuration in NetJSON DeviceConfiguration format", + verbose_name="configuration", + ), + ), + ] diff --git a/tests/openwisp2/sample_connection/migrations/0001_initial.py b/tests/openwisp2/sample_connection/migrations/0001_initial.py index ab73a253d..a030b5e4f 100644 --- a/tests/openwisp2/sample_connection/migrations/0001_initial.py +++ b/tests/openwisp2/sample_connection/migrations/0001_initial.py @@ -1,12 +1,10 @@ # Generated by Django 3.0.6 on 2020-05-10 18:11 -import collections import uuid import django import django.db.models.deletion import django.utils.timezone -import jsonfield.fields import model_utils.fields import swapper from django.conf import settings @@ -70,11 +68,9 @@ class Migration(migrations.Migration): ), ( "params", - jsonfield.fields.JSONField( + models.JSONField( default=dict, - dump_kwargs={"indent": 4}, help_text="global connection parameters", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="parameters", ), ), @@ -155,15 +151,13 @@ class Migration(migrations.Migration): ("enabled", models.BooleanField(db_index=True, default=True)), ( "params", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=dict, - dump_kwargs={"indent": 4}, help_text=( "local connection parameters (will override the " "global parameters if specified)" ), - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="parameters", ), ), @@ -256,10 +250,8 @@ class Migration(migrations.Migration): ), ( "input", - jsonfield.fields.JSONField( + models.JSONField( blank=True, - dump_kwargs={"indent": 4}, - load_kwargs={"object_pairs_hook": collections.OrderedDict}, null=True, ), ), diff --git a/tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py b/tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py new file mode 100644 index 000000000..f74802cae --- /dev/null +++ b/tests/openwisp2/sample_connection/migrations/0004_replace_jsonfield_with_django_builtin.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.11 on 2026-02-06 06:11 + +import django.core.serializers.json +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sample_connection", "0003_name_unique_per_organization"), + ] + + operations = [ + migrations.AlterField( + model_name="command", + name="input", + field=models.JSONField( + blank=True, + encoder=django.core.serializers.json.DjangoJSONEncoder, + null=True, + ), + ), + migrations.AlterField( + model_name="credentials", + name="params", + field=models.JSONField( + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text="global connection parameters", + verbose_name="parameters", + ), + ), + migrations.AlterField( + model_name="deviceconnection", + name="params", + field=models.JSONField( + blank=True, + default=dict, + encoder=django.core.serializers.json.DjangoJSONEncoder, + help_text=( + "local connection parameters (will override the global " + "parameters if specified)" + ), + verbose_name="parameters", + ), + ), + ] diff --git a/tests/openwisp2/sample_pki/migrations/0001_initial.py b/tests/openwisp2/sample_pki/migrations/0001_initial.py index 2b7ec4020..6db6ad3db 100644 --- a/tests/openwisp2/sample_pki/migrations/0001_initial.py +++ b/tests/openwisp2/sample_pki/migrations/0001_initial.py @@ -1,11 +1,9 @@ # Generated by Django 3.0.6 on 2020-05-10 18:14 -import collections import django.db.models.deletion import django.utils.timezone import django_x509.base.models -import jsonfield.fields import model_utils.fields import swapper from django.conf import settings @@ -128,12 +126,10 @@ class Migration(migrations.Migration): ), ( "extensions", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=list, - dump_kwargs={"indent": 4}, help_text="additional x509 certificate extensions", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="extensions", ), ), @@ -306,12 +302,10 @@ class Migration(migrations.Migration): ), ( "extensions", - jsonfield.fields.JSONField( + models.JSONField( blank=True, default=list, - dump_kwargs={"indent": 4}, help_text="additional x509 certificate extensions", - load_kwargs={"object_pairs_hook": collections.OrderedDict}, verbose_name="extensions", ), ),