-
-
Notifications
You must be signed in to change notification settings - Fork 87
[change] Replace third-party JSONField with Django built-in JSONField #259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,13 +5,15 @@ | |
|
|
||
| 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, TextField | ||
| from django.db.models.functions import Cast | ||
| from django.db.models.signals import post_delete, post_save | ||
| from django.dispatch import receiver | ||
| from django.utils.functional import cached_property | ||
| from django.utils.timezone import now | ||
| from django.utils.translation import gettext_lazy as _ | ||
| from jsonfield import JSONField | ||
| from rest_framework.utils.encoders import JSONEncoder | ||
|
|
||
| from openwisp_users.mixins import ShareableOrgMixin | ||
|
|
@@ -32,20 +34,18 @@ class AbstractNode(ShareableOrgMixin, TimeStampedEditableModel): | |
| ) | ||
| label = models.CharField(max_length=64, blank=True) | ||
| # netjson ID and local_addresses | ||
| addresses = JSONField(default=[], blank=True) | ||
| addresses = JSONField(default=list, blank=True, encoder=DjangoJSONEncoder) | ||
| properties = JSONField( | ||
| default=dict, | ||
| blank=True, | ||
| load_kwargs={"object_pairs_hook": OrderedDict}, | ||
| dump_kwargs={"indent": 4, "cls": JSONEncoder}, | ||
| encoder=DjangoJSONEncoder, | ||
| ) | ||
| user_properties = JSONField( | ||
| verbose_name=_("user defined properties"), | ||
| help_text=_("If you need to add additional data to this node use this field"), | ||
| default=dict, | ||
| blank=True, | ||
| load_kwargs={"object_pairs_hook": OrderedDict}, | ||
| dump_kwargs={"indent": 4, "cls": JSONEncoder}, | ||
| encoder=DjangoJSONEncoder, | ||
| ) | ||
|
|
||
| class Meta: | ||
|
|
@@ -138,10 +138,13 @@ def get_from_address(cls, address, topology): | |
| :param topology: Topology instance | ||
| :returns: Node object or None | ||
| """ | ||
| address = '"{}"'.format(address) | ||
| return cls.objects.filter( | ||
| topology=topology, addresses__contains=address | ||
| ).first() | ||
| needle = '"{}"'.format(address) | ||
| return ( | ||
| cls.objects.filter(topology=topology) | ||
| .annotate(_addresses_text=Cast("addresses", output_field=TextField())) | ||
| .filter(_addresses_text__contains=needle) | ||
| .first() | ||
| ) | ||
|
Comment on lines
+141
to
+147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win Add comment explaining the quote-wrapping pattern and consider exact-match validation. The 📝 Proposed improvements `@classmethod`
def get_from_address(cls, address, topology):
"""
Find node from one of its addresses and its topology.
:param address: string
:param topology: Topology instance
:returns: Node object or None
"""
+ # Wrap address in quotes to match JSON array element syntax: ["192.168.0.1"]
+ # Cast to text for substring search (required for PostgreSQL jsonb compatibility)
needle = '"{}"'.format(address)
return (
cls.objects.filter(topology=topology)
.annotate(_addresses_text=Cast("addresses", output_field=TextField()))
.filter(_addresses_text__contains=needle)
.first()
)Additionally, consider if substring matching could cause false positives. For example, searching for As per coding guidelines, cryptic or non-obvious code (complex patterns or hard-to-read code) must include a concise comment explaining why it is needed. 🤖 Prompt for AI Agents |
||
|
|
||
| @classmethod | ||
| def count_address(cls, address, topology): | ||
|
|
@@ -151,10 +154,13 @@ def count_address(cls, address, topology): | |
| :param topology: Topology instance | ||
| :returns: int | ||
| """ | ||
| address = '"{}"'.format(address) | ||
| return cls.objects.filter( | ||
| topology=topology, addresses__contains=address | ||
| ).count() | ||
| needle = '"{}"'.format(address) | ||
| return ( | ||
| cls.objects.filter(topology=topology) | ||
| .annotate(_addresses_text=Cast("addresses", output_field=TextField())) | ||
| .filter(_addresses_text__contains=needle) | ||
| .count() | ||
| ) | ||
|
Comment on lines
+157
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win Add comment explaining the quote-wrapping pattern. Similar to 📝 Proposed improvement `@classmethod`
def count_address(cls, address, topology):
"""
Count nodes with the specified address and topology.
:param address: string
:param topology: Topology instance
:returns: int
"""
+ # Wrap address in quotes to match JSON array element syntax: ["192.168.0.1"]
+ # Cast to text for substring search (required for PostgreSQL jsonb compatibility)
needle = '"{}"'.format(address)
return (
cls.objects.filter(topology=topology)
.annotate(_addresses_text=Cast("addresses", output_field=TextField()))
.filter(_addresses_text__contains=needle)
.count()
)As per coding guidelines, cryptic or non-obvious code (complex patterns or hard-to-read code) must include a concise comment explaining why it is needed. 🤖 Prompt for AI Agents |
||
|
|
||
| @classmethod | ||
| def delete_expired_nodes(cls): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,6 +79,10 @@ def handle(self, *args, **options): | |
| for data in netjsongraph_data: | ||
| data["fields"]["organization"] = str(org.pk) | ||
| data["model"] = f'{self.app_label}.{data["model"].split(".")[1]}' | ||
| for json_field in ("addresses", "properties"): | ||
| value = data["fields"].get(json_field) | ||
| if isinstance(value, str): | ||
| data["fields"][json_field] = json.loads(value) | ||
|
Comment on lines
+82
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for JSON parsing. The 🛡️ Proposed fix to add error handling for json_field in ("addresses", "properties"):
value = data["fields"].get(json_field)
if isinstance(value, str):
- data["fields"][json_field] = json.loads(value)
+ try:
+ data["fields"][json_field] = json.loads(value)
+ except json.JSONDecodeError:
+ self.stdout.write(
+ self.style.WARNING(
+ f"Failed to parse {json_field} for {data['model']} "
+ f"(pk: {data['pk']}), using empty default"
+ )
+ )
+ data["fields"][json_field] = [] if json_field == "addresses" else {}🤖 Prompt for AI Agents |
||
| # Save in anotherfile | ||
| with open(f'{options["backup"]}/netjsongraph_loaded.json', "w") as outfile: | ||
| json.dump(netjsongraph_data, outfile) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import django.core.serializers.json | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("topology", "0016_alter_topology_parser"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AlterField( | ||
| model_name="link", | ||
| name="properties", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| encoder=django.core.serializers.json.DjangoJSONEncoder, | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="link", | ||
| name="user_properties", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| encoder=django.core.serializers.json.DjangoJSONEncoder, | ||
| help_text="If you need to add additional data to this link use this field", | ||
| verbose_name="user defined properties", | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="node", | ||
| name="addresses", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=list, | ||
| encoder=django.core.serializers.json.DjangoJSONEncoder, | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="node", | ||
| name="properties", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| encoder=django.core.serializers.json.DjangoJSONEncoder, | ||
| ), | ||
| ), | ||
| migrations.AlterField( | ||
| model_name="node", | ||
| name="user_properties", | ||
| field=models.JSONField( | ||
| blank=True, | ||
| default=dict, | ||
| encoder=django.core.serializers.json.DjangoJSONEncoder, | ||
| help_text="If you need to add additional data to this node use this field", | ||
| verbose_name="user defined properties", | ||
| ), | ||
| ), | ||
| ] | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
Uh oh!
There was an error while loading. Please reload this page.