Skip to content
Closed
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ on:
push:
branches:
- master
- "1.2"
pull_request:
branches:
- master
- "1.1"
- gsoc25-map
- gsoc25-whois
- "1.2"

jobs:
build:
Expand Down Expand Up @@ -74,7 +74,7 @@ jobs:
libgeos-dev libspatialite-dev spatialite-bin \
libsqlite3-mod-spatialite
sudo npm install -g prettier
pip install -U pip wheel setuptools
pip install -U pip wheel "setuptools<82"
pip install -U -r requirements-test.txt
pip install -U -e .
pip install ${{ matrix.django-version }}
Expand Down
36 changes: 36 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
Changelog
=========

Version 1.2.3 [2026-04-09]
--------------------------
Comment on lines +4 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# Read the CHANGES.rst file to see the actual content
cat -n CHANGES.rst | head -30

Repository: openwisp/openwisp-controller

Length of output: 918


🏁 Script executed:

# Check git log to understand PR creation date and commit details
git log --oneline -20

Repository: openwisp/openwisp-controller

Length of output: 132


🏁 Script executed:

# Check if there's git metadata that shows when this was created
git log --all --date=short --format='%h %ad %s' | head -20

Repository: openwisp/openwisp-controller

Length of output: 1648


Clarify the release date for version 1.2.3.

The changelog shows version 1.2.3 dated [2026-04-09], but recent commits in the repository are dated 2026-05-19 (approximately 6 weeks later). Please confirm whether this date reflects:

  • An intentional backdated release for historical record-keeping
  • An advance changelog entry written before the actual release
  • The actual release date that should be updated to the current date
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGES.rst` around lines 4 - 5, Update the release date for "Version 1.2.3"
in CHANGES.rst (the line containing "Version 1.2.3 [2026-04-09]") to reflect the
correct intent: either change the bracketed date to the actual release date
(e.g., current date), annotate it as an "anticipated" or "backdated" entry by
adding a short parenthetical note, or add a one-line comment below the heading
clarifying that the entry was written earlier than the release; pick the
appropriate approach and make the single-line change so the date or note
unambiguously communicates whether it was backdated, pre-written, or should be
updated to the real release date.


Bugfixes
~~~~~~~~

- Corrected initial field value assignment in ``AbstractDevice`` class
- Fixed duplicate template entries in Device admin
- Invalidate Config checksum after subnet provisioning
- Fixed concurrent ``update_config`` detection `#1204
<https://github.com/openwisp/openwisp-controller/issues/1204>`_

Version 1.2.2 [2026-03-06]
--------------------------

Changes
~~~~~~~

Other changes
+++++++++++++

- Improved help text of configuration variable fields
- Minor fixes in the test suite

Version 1.2.1 [2026-03-04]
--------------------------

Bugfixes
~~~~~~~~

- Use context variables in Vpn.auto_client for OpenVPN backend
- Fixed 500 FieldError in DeviceLocationView `#1110
<https://github.com/openwisp/openwisp-controller/issues/1110>`_
- Fixed MultiValueDictKeyError on empty device form submission `#1057
<https://github.com/openwisp/openwisp-controller/issues/1057>`_

Version 1.2.0 [2025-10-24]
--------------------------

Expand Down
2 changes: 1 addition & 1 deletion openwisp_controller/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION = (1, 2, 0, "final")
VERSION = (1, 2, 3, "final")
__version__ = VERSION # alias


Expand Down
15 changes: 12 additions & 3 deletions openwisp_controller/config/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,13 @@ def get_temp_model_instance(self, **options):
config_model = self.Meta.model
instance = config_model(**options)
device_model = config_model.device.field.related_model
org = Organization.objects.get(pk=self.data["organization"])
if not (org_id := self.data.get("organization")):
# We cannot validate the templates without an organization.
return
org = Organization.objects.get(pk=org_id)
instance.device = device_model(
name=self.data["name"],
mac_address=self.data["mac_address"],
name=self.data.get("name", ""),
mac_address=self.data.get("mac_address", ""),
organization=org,
)
return instance
Expand All @@ -369,6 +372,12 @@ def clean_templates(self):
# when adding self.instance is empty, we need to create a
# temporary instance that we'll use just for validation
config = self.get_temp_model_instance(**data)
if not config:
# The request does not contain vaild data to create a temporary
# Device instance. Thus, we cannot validate the templates.
# The Device validation will be handled by DeviceAdmin.
# Therefore, we don't need to raise any error here.
return
else:
config = self.instance
if config.backend and templates:
Expand Down
8 changes: 4 additions & 4 deletions openwisp_controller/config/base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@ class AbstractConfig(ChecksumCacheMixin, BaseConfig):
blank=True,
default=dict,
help_text=_(
"Additional "
'<a href="http://netjsonconfig.openwisp.org/'
'en/stable/general/basics.html#context" target="_blank">'
"context (configuration variables)</a> in JSON format"
"allows overriding "
'<a href="https://openwisp.io/docs/stable/controller/user/variables.html'
'" target="_blank">'
"configuration variables</a>"
),
load_kwargs={"object_pairs_hook": collections.OrderedDict},
dump_kwargs={"indent": 4},
Expand Down
6 changes: 3 additions & 3 deletions openwisp_controller/config/base/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,9 @@ def _get_initial_values_for_checked_fields(self):
if not present_values:
return
self.refresh_from_db(fields=present_values.keys())
for field in self._changed_checked_fields:
setattr(self, f"_initial_{field}", field)
setattr(self, field, present_values[field])
for field, value in present_values.items():
setattr(self, f"_initial_{field}", getattr(self, field))
setattr(self, field, value)

def _check_name_changed(self):
if self._initial_name == models.DEFERRED:
Expand Down
8 changes: 4 additions & 4 deletions openwisp_controller/config/base/device_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ class AbstractDeviceGroup(OrgMixin, TimeStampedEditableModel):
load_kwargs={"object_pairs_hook": collections.OrderedDict},
dump_kwargs={"indent": 4},
help_text=_(
"Group meta data, use this field to store data which is related"
" to this group and can be retrieved via the REST API."
"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"),
)
Expand All @@ -53,8 +54,7 @@ class AbstractDeviceGroup(OrgMixin, TimeStampedEditableModel):
load_kwargs={"object_pairs_hook": collections.OrderedDict},
dump_kwargs={"indent": 4},
help_text=_(
"This field can be used to add meta data for the group"
' or to add "Configuration Variables" to the devices.'
"Define configuration variables available to all devices in this group"
),
verbose_name=_("Configuration Variables"),
)
Expand Down
3 changes: 2 additions & 1 deletion openwisp_controller/config/base/multitenancy.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class AbstractOrganizationConfigSettings(UUIDModel):
load_kwargs={"object_pairs_hook": collections.OrderedDict},
dump_kwargs={"indent": 4},
help_text=_(
'This field can be used to add "Configuration Variables"' " to the devices."
"Define reusable configuration variables available "
"to all devices in this organization"
),
verbose_name=_("Configuration Variables"),
)
Expand Down
7 changes: 3 additions & 4 deletions openwisp_controller/config/base/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,9 @@ class AbstractTemplate(ShareableOrgMixinUniqueName, BaseConfig):
default=dict,
blank=True,
help_text=_(
"A dictionary containing the default "
"values for the variables used by this "
"template; these default variables will "
"be used during schema validation."
"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": OrderedDict},
dump_kwargs={"indent": 4},
Expand Down
2 changes: 1 addition & 1 deletion openwisp_controller/config/base/vpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ def auto_client(self, auto_cert=True, template_backend_class=None):
context_keys.pop("ip_address", None)
context_keys.pop("vpn_subnet", None)
auto = backend.auto_client(
host=self.host,
host=vpn_host,
server=self.config[config_dict_key][0],
**context_keys,
)
Expand Down
7 changes: 4 additions & 3 deletions openwisp_controller/config/migrations/0023_update_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ class Migration(migrations.Migration):
default=dict,
dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text=(
'Additional <a href="http://netjsonconfig.openwisp.org'
'/en/stable/general/basics.html#context" target="_blank">'
"context (configuration variables)</a> in JSON format"
"allows overriding "
'<a href="https://openwisp.io/docs/stable/controller/user/variables.html' # noqa: E501
'" target="_blank">'
"configuration variables</a>"
),
load_kwargs={"object_pairs_hook": collections.OrderedDict},
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class Migration(migrations.Migration):
default=dict,
dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text=(
"A dictionary containing the default values for "
"the variables used by this template; these default "
"variables will be used during schema validation."
"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",
Expand Down
8 changes: 5 additions & 3 deletions openwisp_controller/config/migrations/0036_device_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ class Migration(migrations.Migration):
dump_kwargs={"ensure_ascii": False, "indent": 4},
load_kwargs={"object_pairs_hook": collections.OrderedDict},
help_text=(
"Group meta data, use this field to store data which is"
" related to this group and can be retrieved via the"
" REST API."
"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",
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class Migration(migrations.Migration):
default=dict,
dump_kwargs={"ensure_ascii": False, "indent": 4},
help_text=(
"This field can be used to add meta data for the group"
' or to add "Configuration Variables" to the devices.'
"Define configuration variables available "
"to all devices in this group"
),
load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="Configuration Variables",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class Migration(migrations.Migration):
default=dict,
dump_kwargs={"indent": 4},
help_text=(
'This field can be used to add "Configuration Variables"'
" to the devices."
"Define reusable configuration variables available "
"to all devices in this organization"
),
load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="Configuration Variables",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Generated by Django for issue #1113 optimization

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


def populate_checksum_db(apps, schema_editor):
Expand All @@ -13,7 +12,7 @@ def populate_checksum_db(apps, schema_editor):
hence we use Config.objects.bulk_update() instead of
Config.update_status_if_checksum_changed().
"""
Config = load_model("config", "Config")
Config = apps.get_model("config", "Config")
chunk_size = 100
updated_configs = []
qs = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ django.jQuery(function ($) {
resetTemplateOptions();
var enabledTemplates = [],
sortedm2mUl = $("ul.sortedm2m-items:first"),
sortedm2mPrefixUl = $("ul.sortedm2m-items:last");
sortedm2mPrefixUl = $("#config-empty ul.sortedm2m-items");

// Adds "li" elements for templates
Object.keys(data).forEach(function (templateId, index) {
Expand Down
30 changes: 30 additions & 0 deletions openwisp_controller/config/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2279,6 +2279,36 @@ def test_templates_fetch_queries_10(self):
config = self._create_config(organization=self._get_org())
self._verify_template_queries(config, 10)

def test_empty_device_form_with_config_inline(self):
org = self._get_org()
template = self._create_template(organization=org)
path = reverse(f"admin:{self.app_label}_device_add")
# Submit form without required device fields but with config inline
# This reproduces the scenario where user clicks "Add another Configuration"
# and submits without filling device details
params = {
"config-0-backend": "netjsonconfig.OpenWrt",
"config-0-templates": str(template.pk),
"config-0-config": json.dumps({}),
"config-0-context": "",
"config-TOTAL_FORMS": 1,
"config-INITIAL_FORMS": 0,
"config-MIN_NUM_FORMS": 0,
"config-MAX_NUM_FORMS": 1,
"deviceconnection_set-TOTAL_FORMS": 0,
"deviceconnection_set-INITIAL_FORMS": 0,
"deviceconnection_set-MIN_NUM_FORMS": 0,
"deviceconnection_set-MAX_NUM_FORMS": 1000,
"command_set-TOTAL_FORMS": 0,
"command_set-INITIAL_FORMS": 0,
"command_set-MIN_NUM_FORMS": 0,
"command_set-MAX_NUM_FORMS": 1000,
}
response = self.client.post(path, params)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "errorlist")
self.assertEqual(Device.objects.count(), 0)


class TestTransactionAdmin(
CreateConfigTemplateMixin,
Expand Down
14 changes: 14 additions & 0 deletions openwisp_controller/config/tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,20 @@ def test_device_field_changed_checks(self):
with self.assertNumQueries(3):
device._check_changed_fields()

def test_deferred_fields_populated_correctly(self):
device = self._create_device(
name="deferred-test",
management_ip="10.0.0.1",
)
# Load the instance with deferred fields omitted
device = Device.objects.only("id").get(pk=device.pk)
device.management_ip = "10.0.0.55"
# Saving the device object will populate the deferred fields
device.save()
# Ensure `_initial_<field>` contains the actual value, not the field name
self.assertEqual(getattr(device, "_initial_management_ip"), "10.0.0.55")
self.assertNotEqual(getattr(device, "_initial_management_ip"), "management_ip")

def test_exceed_organization_device_limit(self):
org = self._get_org()
org.config_limits.device_limit = 1
Expand Down
Loading
Loading