Skip to content

Commit 31f37dd

Browse files
authored
[change:radius] Replace third-party JSONField with Django built-in JSONField #600
Replaced jsonfield package's JSONField with Django's built-in JSONField in RadiusBatch.user_credentials and OrganizationRadiusSettings.sms_meta_data. Rewrote the two historical migrations that referenced jsonfield.fields (0007_sms_verification, 0009_radbatch_user_credentials_field) to use django.db.models.JSONField directly, allowing the jsonfield package to be removed from setup.py. RadiusBatch.prefix_add now assigns user_credentials as a Python list directly instead of json.dumps()-ing it first. Migration 0044 converts existing double-encoded JSON strings in user_credentials to proper JSON values, iterating with .iterator() for memory efficiency. Closes #600
1 parent 9db2867 commit 31f37dd

9 files changed

Lines changed: 196 additions & 15 deletions

openwisp_radius/base/models.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import csv
22
import ipaddress
3-
import json
43
import logging
54
import os
65
import string
@@ -17,13 +16,13 @@
1716
from django.core.cache import cache
1817
from django.core.exceptions import ObjectDoesNotExist, ValidationError
1918
from django.core.mail import send_mail
19+
from django.core.serializers.json import DjangoJSONEncoder
2020
from django.db import models, transaction
21-
from django.db.models import ProtectedError, Q
21+
from django.db.models import JSONField, ProtectedError, Q
2222
from django.utils import timezone
2323
from django.utils.crypto import get_random_string
2424
from django.utils.timezone import now
2525
from django.utils.translation import gettext_lazy as _
26-
from jsonfield import JSONField
2726
from model_utils.fields import AutoLastModifiedField
2827
from openwisp_notifications.signals import notify
2928
from phonenumber_field.modelfields import PhoneNumberField
@@ -928,6 +927,7 @@ class AbstractRadiusBatch(OrgMixin, TimeStampedEditableModel):
928927
null=True,
929928
blank=True,
930929
verbose_name="PDF",
930+
encoder=DjangoJSONEncoder,
931931
)
932932
expiration_date = models.DateField(
933933
verbose_name=_("expiration date"),
@@ -1023,7 +1023,7 @@ def prefix_add(self, prefix, n, password_length=BATCH_DEFAULT_PASSWORD_LENGTH):
10231023
for user in users_list:
10241024
user.full_clean()
10251025
self.save_user(user)
1026-
self.user_credentials = json.dumps(user_credentials)
1026+
self.user_credentials = user_credentials
10271027
self.full_clean()
10281028
self.save()
10291029

@@ -1247,6 +1247,7 @@ class AbstractOrganizationRadiusSettings(UUIDModel):
12471247
" (optional, leave blank if unsure)"
12481248
),
12491249
verbose_name=_("SMS meta data"),
1250+
encoder=DjangoJSONEncoder,
12501251
)
12511252
freeradius_allowed_hosts = FallbackTextField(
12521253
help_text=_GET_IP_LIST_HELP_TEXT,

openwisp_radius/migrations/0007_sms_verification.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import django.db.models.deletion
44
import django.utils.timezone
5-
import jsonfield.fields
65
import model_utils.fields
76
import phonenumber_field.modelfields
87
from django.conf import settings
@@ -47,7 +46,7 @@ class Migration(migrations.Migration):
4746
migrations.AddField(
4847
model_name="organizationradiussettings",
4948
name="sms_meta_data",
50-
field=jsonfield.fields.JSONField(
49+
field=models.JSONField(
5150
blank=True,
5251
help_text=(
5352
"Additional configuration for SMS backend in JSON format"

openwisp_radius/migrations/0009_radbatch_user_credentials_field.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Generated by Django 3.0.7 on 2020-06-08 13:52
22

3-
import jsonfield.fields
4-
from django.db import migrations
3+
from django.db import migrations, models
54

65

76
class Migration(migrations.Migration):
@@ -17,7 +16,7 @@ class Migration(migrations.Migration):
1716
migrations.AddField(
1817
model_name="radiusbatch",
1918
name="user_credentials",
20-
field=jsonfield.fields.JSONField(
19+
field=models.JSONField(
2120
blank=True,
2221
null=True,
2322
verbose_name="PDF",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Generated by Django 5.2.5 on 2025-10-22 18:34
2+
3+
import django.core.serializers.json
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("openwisp_radius", "0042_set_existing_batches_completed"),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name="organizationradiussettings",
16+
name="sms_meta_data",
17+
field=models.JSONField(
18+
blank=True,
19+
encoder=django.core.serializers.json.DjangoJSONEncoder,
20+
help_text=(
21+
"Additional configuration for SMS backend in JSON format "
22+
"(optional, leave blank if unsure)"
23+
),
24+
null=True,
25+
verbose_name="SMS meta data",
26+
),
27+
),
28+
migrations.AlterField(
29+
model_name="radiusbatch",
30+
name="user_credentials",
31+
field=models.JSONField(
32+
blank=True,
33+
encoder=django.core.serializers.json.DjangoJSONEncoder,
34+
null=True,
35+
verbose_name="PDF",
36+
),
37+
),
38+
]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Generated by Django 5.2.9 on 2026-02-13 18:15
2+
3+
import json
4+
import logging
5+
6+
from django.db import migrations
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def convert_user_credentials_data(apps, schema_editor):
12+
"""
13+
Convert existing double-encoded JSON strings in user_credentials field
14+
to proper JSON objects for Django's built-in JSONField.
15+
"""
16+
db_alias = schema_editor.connection.alias
17+
RadiusBatch = apps.get_model("openwisp_radius", "RadiusBatch")
18+
for batch in (
19+
RadiusBatch.objects.using(db_alias)
20+
.exclude(user_credentials__isnull=True)
21+
.iterator()
22+
):
23+
if isinstance(batch.user_credentials, str):
24+
try:
25+
batch.user_credentials = json.loads(batch.user_credentials)
26+
batch.save(using=db_alias, update_fields=["user_credentials"])
27+
except Exception as e:
28+
logger.exception(f"Encountered error while processing {batch}: {e}")
29+
print(f"Encountered error while processing {batch}: {e}")
30+
31+
32+
class Migration(migrations.Migration):
33+
34+
dependencies = [
35+
(
36+
"openwisp_radius",
37+
"0043_alter_organizationradiussettings_sms_meta_data_and_more",
38+
),
39+
]
40+
41+
operations = [
42+
migrations.RunPython(
43+
convert_user_credentials_data, reverse_code=migrations.RunPython.noop
44+
),
45+
]
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import importlib
2+
import json
3+
from unittest.mock import MagicMock
4+
5+
from django.db import connection
6+
from django.test import TestCase
7+
8+
from openwisp_users.tests.utils import TestOrganizationMixin
9+
10+
from ..utils import load_model
11+
12+
RadiusBatch = load_model("RadiusBatch")
13+
14+
migration_module = importlib.import_module(
15+
"openwisp_radius.migrations.0044_convert_user_credentials_data"
16+
)
17+
convert_user_credentials_data = migration_module.convert_user_credentials_data
18+
19+
20+
class Test0044Migration(TestOrganizationMixin, TestCase):
21+
def test_convert_user_credentials_data(self):
22+
org = self._get_org()
23+
batch = RadiusBatch.objects.create(
24+
name="test_batch_migration",
25+
strategy="prefix",
26+
prefix="test",
27+
organization=org,
28+
)
29+
RadiusBatch.objects.filter(pk=batch.pk).update(
30+
user_credentials=json.dumps({"user1": "pass1"})
31+
)
32+
33+
apps = MagicMock()
34+
apps.get_model.return_value = RadiusBatch
35+
36+
schema_editor = MagicMock()
37+
schema_editor.connection = connection
38+
39+
convert_user_credentials_data(apps, schema_editor)
40+
41+
batch.refresh_from_db()
42+
self.assertEqual(batch.user_credentials, {"user1": "pass1"})
43+
44+
def test_convert_user_credentials_data_invalid_json(self):
45+
org = self._get_org()
46+
batch = RadiusBatch.objects.create(
47+
name="test_batch_invalid",
48+
strategy="prefix",
49+
prefix="test2",
50+
organization=org,
51+
)
52+
RadiusBatch.objects.filter(pk=batch.pk).update(
53+
user_credentials="invalid_json_string"
54+
)
55+
56+
apps = MagicMock()
57+
apps.get_model.return_value = RadiusBatch
58+
59+
schema_editor = MagicMock()
60+
schema_editor.connection = connection
61+
62+
convert_user_credentials_data(apps, schema_editor)
63+
64+
batch.refresh_from_db()
65+
self.assertEqual(batch.user_credentials, "invalid_json_string")

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@
5353
"weasyprint>=65,<68",
5454
"dj-rest-auth>=6.0,<7.3",
5555
"django-sendsms~=0.5.0",
56-
"jsonfield~=3.1.0",
5756
"django-private-storage~=3.1.0",
5857
"django-ipware>=5.0,<7.1",
5958
"pyrad~=2.4",

tests/openwisp2/sample_radius/migrations/0002_initial_openwisp_app.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import django.core.validators
88
import django.db.models.deletion
99
import django.utils.timezone
10-
import jsonfield.fields
1110
import model_utils.fields
1211
import private_storage.fields
1312
import private_storage.storage.files
@@ -543,7 +542,7 @@ class Migration(migrations.Migration):
543542
),
544543
(
545544
"sms_meta_data",
546-
jsonfield.fields.JSONField(
545+
models.JSONField(
547546
blank=True,
548547
help_text=(
549548
"Additional configuration for SMS backend in JSON format"
@@ -695,9 +694,7 @@ class Migration(migrations.Migration):
695694
),
696695
(
697696
"user_credentials",
698-
jsonfield.fields.JSONField(
699-
blank=True, null=True, verbose_name="PDF"
700-
),
697+
models.JSONField(blank=True, null=True, verbose_name="PDF"),
701698
),
702699
(
703700
"expiration_date",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Generated by Django 5.2.9 on 2026-02-13 18:15
2+
3+
import django.core.serializers.json
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("sample_radius", "0031_radiusbatch_status"),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name="organizationradiussettings",
16+
name="sms_meta_data",
17+
field=models.JSONField(
18+
blank=True,
19+
encoder=django.core.serializers.json.DjangoJSONEncoder,
20+
help_text=(
21+
"Additional configuration for SMS backend in JSON format "
22+
"(optional, leave blank if unsure)"
23+
),
24+
null=True,
25+
verbose_name="SMS meta data",
26+
),
27+
),
28+
migrations.AlterField(
29+
model_name="radiusbatch",
30+
name="user_credentials",
31+
field=models.JSONField(
32+
blank=True,
33+
encoder=django.core.serializers.json.DjangoJSONEncoder,
34+
null=True,
35+
verbose_name="PDF",
36+
),
37+
),
38+
]

0 commit comments

Comments
 (0)