Skip to content
Open
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
7 changes: 6 additions & 1 deletion docs/developer/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ Once you have created the models, add the following to your
GEO_LOCATION_MODEL = "sample_geo.Location"
GEO_FLOORPLAN_MODEL = "sample_geo.FloorPlan"
GEO_DEVICELOCATION_MODEL = "sample_geo.DeviceLocation"
GEO_ORGANIZATIONGEOSETTINGS_MODEL = "sample_geo.OrganizationGeoSettings"
CONNECTION_CREDENTIALS_MODEL = "sample_connection.Credentials"
CONNECTION_DEVICECONNECTION_MODEL = "sample_connection.DeviceConnection"
CONNECTION_COMMAND_MODEL = "sample_connection.Command"
Expand Down Expand Up @@ -456,7 +457,11 @@ For example:

.. code-block:: python

from openwisp_controller.geo.admin import FloorPlanAdmin, LocationAdmin
from openwisp_controller.geo.admin import (
FloorPlanAdmin,
LocationAdmin,
GeoSettingsInline,
)

FloorPlanAdmin.fields += ["example"] # <-- monkey patching example

Expand Down
28 changes: 28 additions & 0 deletions docs/developer/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,31 @@ The signal is emitted when the peers of VPN server gets changed.

It is only emitted for ``Vpn`` object with **WireGuard** or **VXLAN over
WireGuard** backend.

``whois_fetched``
~~~~~~~~~~~~~~~~~

**Path**: ``openwisp_controller.config.signals.whois_fetched``

**Arguments**:

- ``whois``: instance of ``WHOISInfo`` that was created or updated
- ``updated_fields``: list of fields updated in the lookup
- ``device``: optional instance of ``Device`` related to this WHOIS lookup

This signal is emitted when a WHOIS lookup task successfully creates or
updates a ``WHOISInfo`` record.

``whois_lookup_skipped``
~~~~~~~~~~~~~~~~~~~~~~~~

**Path**: ``openwisp_controller.config.signals.whois_lookup_skipped``

**Arguments**:

- ``device``: instance of ``Device`` for which the WHOIS lookup was
skipped

This signal is emitted when a WHOIS lookup is not triggered because the
lookup conditions were not met (for example, an up-to-date WHOIS record
already exists).
47 changes: 47 additions & 0 deletions docs/user/rest-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,53 @@ device is updating it's position.
},
}'

Get Organization Geographic Settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: text

GET /api/v1/controller/organization/{organization_pk}/geo-settings/

This endpoint allows retrieving geographic settings for a specific
organization.

Update Organization Geographic Settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This endpoint allows updating geographic settings for a specific
organization.

.. code-block:: text

PUT /api/v1/controller/organization/{organization_pk}/geo-settings/

.. code-block:: text

curl -X PUT \
'http://127.0.0.1:8000/api/v1/controller/organization/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/geo-settings/' \
-H 'authorization: Bearer <token>' \
-H 'content-type: application/json' \
-d '{"estimated_location_enabled": true}'

Partially Update Organization Geographic Settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This endpoint allows partial updates of Organization geographic settings
using the PATCH method. PATCH accepts a subset of fields and applies a
partial update to the resource at the same endpoint path.

.. code-block:: text

PATCH /api/v1/controller/organization/{organization_pk}/geo-settings/

.. code-block:: text

curl -X PATCH \
'http://127.0.0.1:8000/api/v1/controller/organization/8a85cc23-bad5-4c7e-b9f4-ffe298defb5c/geo-settings/' \
-H 'authorization: Bearer <token>' \
-H 'content-type: application/json' \
-d '{"estimated_location_enabled": true}'

List Locations
~~~~~~~~~~~~~~

Expand Down
8 changes: 3 additions & 5 deletions openwisp_controller/config/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,8 +694,7 @@ def change_group(self, request, queryset):
"action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
"opts": self.model._meta,
"changelist_url": (
f"{request.resolver_match.app_name}:"
f"{request.resolver_match.url_name}"
f"{request.resolver_match.app_name}:{request.resolver_match.url_name}"
),
}

Expand Down Expand Up @@ -1104,8 +1103,7 @@ def save_clones(view, user, queryset, organization=None):
validated_org = Organization.objects.get(pk=organization)
except (ValidationError, Organization.DoesNotExist) as e:
logger.warning(
"Detected tampering in clone template "
f"form by user {user}: {e}"
f"Detected tampering in clone template form by user {user}: {e}"
)
return
if not user.is_superuser and not user.is_manager(organization):
Expand Down Expand Up @@ -1393,7 +1391,7 @@ def get_fields(self, request, obj=None):
if app_settings.REGISTRATION_ENABLED:
fields += ["registration_enabled", "shared_secret"]
if app_settings.WHOIS_CONFIGURED:
fields += ["whois_enabled", "estimated_location_enabled"]
fields += ["whois_enabled"]
fields += ["context"]
return fields

Expand Down
14 changes: 0 additions & 14 deletions openwisp_controller/config/base/multitenancy.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ class AbstractOrganizationConfigSettings(UUIDModel):
fallback=app_settings.WHOIS_ENABLED,
verbose_name=_("WHOIS Enabled"),
)
estimated_location_enabled = FallbackBooleanChoiceField(
help_text=_("Whether the estimated location feature is enabled"),
fallback=app_settings.ESTIMATED_LOCATION_ENABLED,
verbose_name=_("Estimated Location Enabled"),
)
context = JSONField(
blank=True,
default=dict,
Expand Down Expand Up @@ -77,15 +72,6 @@ def clean(self):
)
}
)
if not self.whois_enabled and self.estimated_location_enabled:
raise ValidationError(
{
"estimated_location_enabled": _(
"Estimated Location feature requires "
"WHOIS Lookup feature to be enabled."
)
}
)
return super().clean()

def save(
Expand Down
13 changes: 0 additions & 13 deletions openwisp_controller/config/base/whois.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,3 @@ def _location_name(self):
}
# Use named placeholder for consistency
return _("Estimated Location: %(ip)s") % {"ip": self.ip_address}

def _get_defaults_for_estimated_location(self):
"""
Used to get default values for creating or updating
an estimated location based on the WHOIS information.
"""
return {
"name": self._location_name,
"type": "outdoor",
"is_mobile": False,
"geometry": self.coordinates,
"address": self.formatted_address,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.12 on 2026-03-23 14:59

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
(
"config",
"0063_organizationconfigsettings_estimated_location_enabled_and_more",
),
# This dependency on the geo app is required to ensure that
# the data from OrganizationConfigSettings is properly migrated to GeoSettings
# before we remove the field.
("geo", "0006_create_geo_settings_for_existing_orgs"),
]
Comment on lines +6 to +16
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 | 🟠 Major

❓ Verification inconclusive

Script executed:

#!/bin/bash
# Check if geo migration 0006 actually migrates the estimated_location_enabled value
cat openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

# Generated by Django 5.2.12 on 2026-03-23 14:59

from django.db import migrations

from . import assign_geo_settings_permissions_to_groups


def create_geo_settings_for_existing_orgs(apps, schema_editor):
    """
    Create OrganizationGeoSettings for all existing organizations
    that don't have one yet.
    """
    Organization = apps.get_model("openwisp_users", "Organization")
    OrganizationGeoSettings = apps.get_model("geo", "OrganizationGeoSettings")

    for org in Organization.objects.all():
        OrganizationGeoSettings.objects.get_or_create(organization_id=org.pk)


class Migration(migrations.Migration):

    dependencies = [
        ("geo", "0005_organizationgeosettings"),
        ("openwisp_users", "0021_rename_user_id_email_openwisp_us_id_06c07a_idx"),
    ]

    operations = [
        migrations.RunPython(
            assign_geo_settings_permissions_to_groups,
            reverse_code=migrations.RunPython.noop,
        ),
        migrations.RunPython(
            create_geo_settings_for_existing_orgs,
            reverse_code=migrations.RunPython.noop,
        ),
    ]

Script executed:

# Search for OrganizationGeoSettings model definition
fd -e py -path "*/geo/*" | xargs rg -l "OrganizationGeoSettings" | head -5

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

tests/openwisp2/settings.py
tests/openwisp2/sample_geo/models.py
docs/developer/extending.rst
openwisp_controller/geo/apps.py
openwisp_controller/geo/models.py

stderr:

error: 'h' isn't a valid value for '--type <filetype>'
  [possible values: file, directory, symlink, executable, empty, socket, pipe]

For more information try '--help'

Script executed:

# Check geo migration 0005 to see what fields are created for OrganizationGeoSettings
cat openwisp_controller/geo/migrations/0005_organizationgeosettings.py

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

# Generated by Django 5.2.12 on 2026-03-23 14:59

import uuid

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

import openwisp_utils.fields


class Migration(migrations.Migration):

    dependencies = [
        ("geo", "0004_location_is_estimated"),
        ("openwisp_users", "0021_rename_user_id_email_openwisp_us_id_06c07a_idx"),
    ]

    operations = [
        migrations.CreateModel(
            name="OrganizationGeoSettings",
            fields=[
                (
                    "id",
                    models.UUIDField(
                        default=uuid.uuid4,
                        editable=False,
                        primary_key=True,
                        serialize=False,
                    ),
                ),
                (
                    "estimated_location_enabled",
                    openwisp_utils.fields.FallbackBooleanChoiceField(
                        blank=True,
                        default=None,
                        fallback=False,
                        help_text="Whether the estimated location feature is enabled",
                        null=True,
                        verbose_name="Estimated Location Enabled",
                    ),
                ),
                (
                    "organization",
                    models.OneToOneField(
                        on_delete=django.db.models.deletion.CASCADE,
                        related_name="geo_settings",
                        to="openwisp_users.organization",
                        verbose_name="organization",
                    ),
                ),
            ],
            options={
                "verbose_name": "Geographic settings",
                "verbose_name_plural": "Geographic settings",
                "abstract": False,
                "swappable": swapper.swappable_setting(
                    "geo", "OrganizationGeoSettings"
                ),
            },
        ),
    ]

Script executed:

# Check if config's OrganizationConfigSettings has estimated_location_enabled
rg "class OrganizationConfigSettings" -A 30 openwisp_controller/config/

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

openwisp_controller/config/models.py:class OrganizationConfigSettings(AbstractOrganizationConfigSettings):
openwisp_controller/config/models.py-    """
openwisp_controller/config/models.py-    Configuration management settings
openwisp_controller/config/models.py-    specific to each organization
openwisp_controller/config/models.py-    """
openwisp_controller/config/models.py-
openwisp_controller/config/models.py-    class Meta(AbstractOrganizationConfigSettings.Meta):
openwisp_controller/config/models.py-        abstract = False
openwisp_controller/config/models.py-        swappable = swapper.swappable_setting("config", "OrganizationConfigSettings")
openwisp_controller/config/models.py-
openwisp_controller/config/models.py-
openwisp_controller/config/models.py-class OrganizationLimits(AbstractOrganizationLimits):
openwisp_controller/config/models.py-    """
openwisp_controller/config/models.py-    Number of allowed devices specific to each organization
openwisp_controller/config/models.py-    """
openwisp_controller/config/models.py-
openwisp_controller/config/models.py-    class Meta(AbstractOrganizationLimits.Meta):
openwisp_controller/config/models.py-        abstract = False
openwisp_controller/config/models.py-        swappable = swapper.swappable_setting("config", "OrganizationLimits")
openwisp_controller/config/models.py-
openwisp_controller/config/models.py-
openwisp_controller/config/models.py-class WHOISInfo(AbstractWHOISInfo):
openwisp_controller/config/models.py-    """
openwisp_controller/config/models.py-    Stores WHOIS information for devices.
openwisp_controller/config/models.py-    """
openwisp_controller/config/models.py-
openwisp_controller/config/models.py-    class Meta(AbstractWHOISInfo.Meta):
openwisp_controller/config/models.py-        abstract = False
openwisp_controller/config/models.py-        swappable = swapper.swappable_setting("config", "WHOISInfo")

Script executed:

# Check config migration 0063 to see when estimated_location_enabled was added
cat openwisp_controller/config/migrations/0063_organizationconfigsettings_estimated_location_enabled_and_more.py

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

# Generated by Django 5.2.1 on 2025-07-10 18:09

import django.contrib.gis.db.models.fields
from django.db import migrations

import openwisp_utils.fields


class Migration(migrations.Migration):

    dependencies = [
        ("config", "0062_whoisinfo"),
    ]

    operations = [
        migrations.AddField(
            model_name="organizationconfigsettings",
            name="estimated_location_enabled",
            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                blank=True,
                default=None,
                fallback=False,
                help_text="Whether the estimated location feature is enabled",
                null=True,
                verbose_name="Estimated Location Enabled",
            ),
        ),
        migrations.AddField(
            model_name="whoisinfo",
            name="coordinates",
            field=django.contrib.gis.db.models.fields.PointField(
                blank=True, help_text="Coordinates", null=True, srid=4326
            ),
        ),
    ]

Script executed:

# Search for AbstractOrganizationConfigSettings to see the field definition
rg "estimated_location_enabled" openwisp_controller/config/ -B 2 -A 2

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py-        (
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py-            "config",
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py:            "0063_organizationconfigsettings_estimated_location_enabled_and_more",
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py-        ),
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py-    ]
--
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py-        migrations.RemoveField(
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py-            model_name="organizationconfigsettings",
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py:            name="estimated_location_enabled",
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py-        ),
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py-    ]
--
openwisp_controller/config/migrations/0063_organizationconfigsettings_estimated_location_enabled_and_more.py-        migrations.AddField(
openwisp_controller/config/migrations/0063_organizationconfigsettings_estimated_location_enabled_and_more.py-            model_name="organizationconfigsettings",
openwisp_controller/config/migrations/0063_organizationconfigsettings_estimated_location_enabled_and_more.py:            name="estimated_location_enabled",
openwisp_controller/config/migrations/0063_organizationconfigsettings_estimated_location_enabled_and_more.py-            field=openwisp_utils.fields.FallbackBooleanChoiceField(
openwisp_controller/config/migrations/0063_organizationconfigsettings_estimated_location_enabled_and_more.py-                blank=True,

Script executed:

# Check the base model to understand the field
rg "class AbstractOrganizationConfigSettings" -A 50 openwisp_controller/config/

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

openwisp_controller/config/base/multitenancy.py:class AbstractOrganizationConfigSettings(UUIDModel):
openwisp_controller/config/base/multitenancy.py-    organization = models.OneToOneField(
openwisp_controller/config/base/multitenancy.py-        swapper.get_model_name("openwisp_users", "Organization"),
openwisp_controller/config/base/multitenancy.py-        verbose_name=_("organization"),
openwisp_controller/config/base/multitenancy.py-        related_name="config_settings",
openwisp_controller/config/base/multitenancy.py-        on_delete=models.CASCADE,
openwisp_controller/config/base/multitenancy.py-    )
openwisp_controller/config/base/multitenancy.py-    registration_enabled = models.BooleanField(
openwisp_controller/config/base/multitenancy.py-        _("auto-registration enabled"),
openwisp_controller/config/base/multitenancy.py-        default=True,
openwisp_controller/config/base/multitenancy.py-        help_text=_("Whether automatic registration of devices is enabled or not"),
openwisp_controller/config/base/multitenancy.py-    )
openwisp_controller/config/base/multitenancy.py-    shared_secret = KeyField(
openwisp_controller/config/base/multitenancy.py-        max_length=32,
openwisp_controller/config/base/multitenancy.py-        unique=True,
openwisp_controller/config/base/multitenancy.py-        db_index=True,
openwisp_controller/config/base/multitenancy.py-        verbose_name=_("shared secret"),
openwisp_controller/config/base/multitenancy.py-        help_text=_("used for automatic registration of devices"),
openwisp_controller/config/base/multitenancy.py-    )
openwisp_controller/config/base/multitenancy.py-    whois_enabled = FallbackBooleanChoiceField(
openwisp_controller/config/base/multitenancy.py-        help_text=_("Whether the WHOIS lookup feature is enabled"),
openwisp_controller/config/base/multitenancy.py-        fallback=app_settings.WHOIS_ENABLED,
openwisp_controller/config/base/multitenancy.py-        verbose_name=_("WHOIS Enabled"),
openwisp_controller/config/base/multitenancy.py-    )
openwisp_controller/config/base/multitenancy.py-    context = JSONField(
openwisp_controller/config/base/multitenancy.py-        blank=True,
openwisp_controller/config/base/multitenancy.py-        default=dict,
openwisp_controller/config/base/multitenancy.py-        load_kwargs={"object_pairs_hook": collections.OrderedDict},
openwisp_controller/config/base/multitenancy.py-        dump_kwargs={"indent": 4},
openwisp_controller/config/base/multitenancy.py-        help_text=_(
openwisp_controller/config/base/multitenancy.py-            "Define reusable configuration variables available "
openwisp_controller/config/base/multitenancy.py-            "to all devices in this organization"
openwisp_controller/config/base/multitenancy.py-        ),
openwisp_controller/config/base/multitenancy.py-        verbose_name=_("Configuration Variables"),
openwisp_controller/config/base/multitenancy.py-    )
openwisp_controller/config/base/multitenancy.py-
openwisp_controller/config/base/multitenancy.py-    class Meta:
openwisp_controller/config/base/multitenancy.py-        verbose_name = _("Configuration management settings")
openwisp_controller/config/base/multitenancy.py-        verbose_name_plural = verbose_name
openwisp_controller/config/base/multitenancy.py-        abstract = True
openwisp_controller/config/base/multitenancy.py-
openwisp_controller/config/base/multitenancy.py-    def __str__(self):
openwisp_controller/config/base/multitenancy.py-        return self.organization.name
openwisp_controller/config/base/multitenancy.py-
openwisp_controller/config/base/multitenancy.py-    def get_context(self):
openwisp_controller/config/base/multitenancy.py-        return deepcopy(self.context)
openwisp_controller/config/base/multitenancy.py-
openwisp_controller/config/base/multitenancy.py-    def clean(self):
openwisp_controller/config/base/multitenancy.py-        if not app_settings.WHOIS_CONFIGURED and self.whois_enabled:
openwisp_controller/config/base/multitenancy.py-            raise ValidationError(
openwisp_controller/config/base/multitenancy.py-                {

Script executed:

# Check the complete migration 0064 to see all operations
cat openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

# Generated by Django 5.2.12 on 2026-03-23 14:59

from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        (
            "config",
            "0063_organizationconfigsettings_estimated_location_enabled_and_more",
        ),
    ]

    operations = [
        migrations.RemoveField(
            model_name="organizationconfigsettings",
            name="estimated_location_enabled",
        ),
    ]

Script executed:

# Check if geo 0006 should have data migration logic - look for similar patterns
rg "RunPython" openwisp_controller/geo/migrations/ -A 5

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py:        migrations.RunPython(
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py-            assign_geo_settings_permissions_to_groups,
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py:            reverse_code=migrations.RunPython.noop,
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py-        ),
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py:        migrations.RunPython(
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py-            create_geo_settings_for_existing_orgs,
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py:            reverse_code=migrations.RunPython.noop,
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py-        ),
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py-    ]
--
openwisp_controller/geo/migrations/0002_default_groups_permissions.py:        migrations.RunPython(
openwisp_controller/geo/migrations/0002_default_groups_permissions.py:            assign_permissions_to_groups, reverse_code=migrations.RunPython.noop
openwisp_controller/geo/migrations/0002_default_groups_permissions.py-        )
openwisp_controller/geo/migrations/0002_default_groups_permissions.py-    ]

Script executed:

# Verify if there's any data migration happening elsewhere
rg "estimated_location_enabled" openwisp_controller/geo/ -B 2 -A 2

Repository: openwisp/openwisp-controller


Repository: openwisp/openwisp-controller
Exit code: 0

stdout:

openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.patch(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.patch(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            self.assertEqual(str(data["organization"]), str(org1.pk))
openwisp_controller/geo/tests/test_api.py-            self.assertEqual(
openwisp_controller/geo/tests/test_api.py:                data["estimated_location_enabled"],
openwisp_controller/geo/tests/test_api.py:                org1_geo_settings.estimated_location_enabled,
openwisp_controller/geo/tests/test_api.py-            )
openwisp_controller/geo/tests/test_api.py-
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.patch(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
openwisp_controller/geo/tests/test_api.py-            self.assertEqual(response.status_code, 200)
openwisp_controller/geo/tests/test_api.py-            org1_geo_settings.refresh_from_db()
openwisp_controller/geo/tests/test_api.py:            self.assertEqual(org1_geo_settings.estimated_location_enabled, False)
openwisp_controller/geo/tests/test_api.py-
openwisp_controller/geo/tests/test_api.py-        with self.subTest("PATCH operation"):
openwisp_controller/geo/tests/test_api.py-            response = self.client.patch(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": True},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
openwisp_controller/geo/tests/test_api.py-            self.assertEqual(response.status_code, 200)
openwisp_controller/geo/tests/test_api.py-            org1_geo_settings.refresh_from_db()
openwisp_controller/geo/tests/test_api.py:            self.assertEqual(org1_geo_settings.estimated_location_enabled, True)
openwisp_controller/geo/tests/test_api.py-
openwisp_controller/geo/tests/test_api.py-        with self.subTest("PUT with organization field should be ignored"):
openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False, "organization": str(org2.pk)},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            org1_geo_settings.refresh_from_db()
openwisp_controller/geo/tests/test_api.py-            self.assertEqual(org1_geo_settings.organization, org1)
openwisp_controller/geo/tests/test_api.py:            self.assertEqual(org1_geo_settings.estimated_location_enabled, False)
openwisp_controller/geo/tests/test_api.py-
openwisp_controller/geo/tests/test_api.py-        with self.subTest("Cannot update geo settings of other organization"):
openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                org2_url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.patch(
openwisp_controller/geo/tests/test_api.py-                org2_url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-                response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                    url,
openwisp_controller/geo/tests/test_api.py:                    {"estimated_location_enabled": True},
openwisp_controller/geo/tests/test_api.py-                    content_type="application/json",
openwisp_controller/geo/tests/test_api.py-                )
openwisp_controller/geo/tests/test_api.py-                self.assertEqual(response.status_code, 400)
openwisp_controller/geo/tests/test_api.py:                self.assertIn("estimated_location_enabled", response.data)
openwisp_controller/geo/tests/test_api.py-                org1_geo_settings.refresh_from_db()
openwisp_controller/geo/tests/test_api.py:                self.assertEqual(org1_geo_settings.estimated_location_enabled, True)
openwisp_controller/geo/tests/test_api.py-
openwisp_controller/geo/tests/test_api.py-                response = self.client.patch(
openwisp_controller/geo/tests/test_api.py-                    url,
openwisp_controller/geo/tests/test_api.py:                    {"estimated_location_enabled": True},
openwisp_controller/geo/tests/test_api.py-                    content_type="application/json",
openwisp_controller/geo/tests/test_api.py-                )
openwisp_controller/geo/tests/test_api.py-                self.assertEqual(response.status_code, 400)
openwisp_controller/geo/tests/test_api.py:                self.assertIn("estimated_location_enabled", response.data)
openwisp_controller/geo/tests/test_api.py-
openwisp_controller/geo/tests/test_api.py-        with self.subTest("Superuser can update any organization's geo settings"):
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                org2_url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": True},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
openwisp_controller/geo/tests/test_api.py-            self.assertEqual(response.status_code, 200)
openwisp_controller/geo/tests/test_api.py-            org2_geo_settings.refresh_from_db()
openwisp_controller/geo/tests/test_api.py:            self.assertEqual(org2_geo_settings.estimated_location_enabled, True)
openwisp_controller/geo/tests/test_api.py-
openwisp_controller/geo/tests/test_api.py-    def test_organization_geo_settings_multi_tenancy(self):
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                org2_url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.patch(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.put(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/tests/test_api.py-            response = self.client.patch(
openwisp_controller/geo/tests/test_api.py-                url,
openwisp_controller/geo/tests/test_api.py:                {"estimated_location_enabled": False},
openwisp_controller/geo/tests/test_api.py-                content_type="application/json",
openwisp_controller/geo/tests/test_api.py-            )
--
openwisp_controller/geo/estimated_location/handlers.py-    except Device.DoesNotExist:
openwisp_controller/geo/estimated_location/handlers.py-        return
openwisp_controller/geo/estimated_location/handlers.py:    if not EstimatedLocationService.check_estimated_location_enabled(
openwisp_controller/geo/estimated_location/handlers.py-        device.organization_id
openwisp_controller/geo/estimated_location/handlers.py-    ):
--
openwisp_controller/geo/migrations/0005_organizationgeosettings.py-                ),
openwisp_controller/geo/migrations/0005_organizationgeosettings.py-                (
openwisp_controller/geo/migrations/0005_organizationgeosettings.py:                    "estimated_location_enabled",
openwisp_controller/geo/migrations/0005_organizationgeosettings.py-                    openwisp_utils.fields.FallbackBooleanChoiceField(
openwisp_controller/geo/migrations/0005_organizationgeosettings.py-                        blank=True,
--
openwisp_controller/geo/estimated_location/service.py-
openwisp_controller/geo/estimated_location/service.py-    `@staticmethod`
openwisp_controller/geo/estimated_location/service.py:    def check_estimated_location_enabled(org_id):
openwisp_controller/geo/estimated_location/service.py-        if not org_id:
openwisp_controller/geo/estimated_location/service.py-            return False
--
openwisp_controller/geo/estimated_location/service.py-
openwisp_controller/geo/estimated_location/service.py-            return geo_app_settings.ESTIMATED_LOCATION_ENABLED
openwisp_controller/geo/estimated_location/service.py:        return org_settings.estimated_location_enabled
openwisp_controller/geo/estimated_location/service.py-
openwisp_controller/geo/estimated_location/service.py-    `@property`
openwisp_controller/geo/estimated_location/service.py:    def is_estimated_location_enabled(self):
openwisp_controller/geo/estimated_location/service.py-        if not config_app_settings.WHOIS_CONFIGURED:
openwisp_controller/geo/estimated_location/service.py-            return False
openwisp_controller/geo/estimated_location/service.py:        return self.check_estimated_location_enabled(self.device.organization_id)
openwisp_controller/geo/estimated_location/service.py-
openwisp_controller/geo/estimated_location/service.py-    def trigger_estimated_location_task(self, ip_address):
--
openwisp_controller/geo/estimated_location/mixins.py-    def to_representation(self, obj):
openwisp_controller/geo/estimated_location/mixins.py-        data = super().to_representation(obj)
openwisp_controller/geo/estimated_location/mixins.py:        if EstimatedLocationService.check_estimated_location_enabled(
openwisp_controller/geo/estimated_location/mixins.py-            obj.organization_id
openwisp_controller/geo/estimated_location/mixins.py-        ):
--
openwisp_controller/geo/estimated_location/mixins.py-    def to_representation(self, obj):
openwisp_controller/geo/estimated_location/mixins.py-        data = super().to_representation(obj)
openwisp_controller/geo/estimated_location/mixins.py:        if EstimatedLocationService.check_estimated_location_enabled(
openwisp_controller/geo/estimated_location/mixins.py-            obj.organization_id
openwisp_controller/geo/estimated_location/mixins.py-        ):
--
openwisp_controller/geo/estimated_location/tests/utils.py-        )
openwisp_controller/geo/estimated_location/tests/utils.py-        # OrganizationGeoSettings is auto-created by signal,
openwisp_controller/geo/estimated_location/tests/utils.py:        # update estimated_location_enabled
openwisp_controller/geo/estimated_location/tests/utils.py:        org.geo_settings.estimated_location_enabled = True
openwisp_controller/geo/estimated_location/tests/utils.py-        org.geo_settings.save()
openwisp_controller/geo/estimated_location/tests/utils.py-
--
openwisp_controller/geo/estimated_location/tests/tests.py-            org = self._get_org()
openwisp_controller/geo/estimated_location/tests/tests.py-            geo_settings = org.geo_settings
openwisp_controller/geo/estimated_location/tests/tests.py:            geo_settings.estimated_location_enabled = True
openwisp_controller/geo/estimated_location/tests/tests.py-            with mock.patch.object(config_app_settings, "WHOIS_CONFIGURED", False):
openwisp_controller/geo/estimated_location/tests/tests.py-                with self.assertRaises(ValidationError) as context_manager:
openwisp_controller/geo/estimated_location/tests/tests.py-                    geo_settings.full_clean()
openwisp_controller/geo/estimated_location/tests/tests.py-                self.assertIn(
openwisp_controller/geo/estimated_location/tests/tests.py:                    "estimated_location_enabled",
openwisp_controller/geo/estimated_location/tests/tests.py-                    context_manager.exception.message_dict,
openwisp_controller/geo/estimated_location/tests/tests.py-                )
--
openwisp_controller/geo/estimated_location/tests/tests.py-            response = self.client.get(url)
openwisp_controller/geo/estimated_location/tests/tests.py-            self.assertContains(
openwisp_controller/geo/estimated_location/tests/tests.py:                response, 'name="geo_settings-0-estimated_location_enabled"'
openwisp_controller/geo/estimated_location/tests/tests.py-            )
openwisp_controller/geo/estimated_location/tests/tests.py-
--
openwisp_controller/geo/estimated_location/tests/tests.py-                response = self.client.get(url)
openwisp_controller/geo/estimated_location/tests/tests.py-                self.assertNotContains(
openwisp_controller/geo/estimated_location/tests/tests.py:                    response, 'name="geo_settings-0-estimated_location_enabled"'
openwisp_controller/geo/estimated_location/tests/tests.py-                )
openwisp_controller/geo/estimated_location/tests/tests.py-
--
openwisp_controller/geo/estimated_location/tests/tests.py-        with self.subTest("WHOIS must be configured to enable estimated location"):
openwisp_controller/geo/estimated_location/tests/tests.py-            geo_settings = org.geo_settings
openwisp_controller/geo/estimated_location/tests/tests.py:            geo_settings.estimated_location_enabled = True
openwisp_controller/geo/estimated_location/tests/tests.py-            with mock.patch.object(config_app_settings, "WHOIS_CONFIGURED", False):
openwisp_controller/geo/estimated_location/tests/tests.py-                with self.assertRaises(ValidationError) as context_manager:
openwisp_controller/geo/estimated_location/tests/tests.py-                    geo_settings.full_clean()
openwisp_controller/geo/estimated_location/tests/tests.py-                self.assertIn(
openwisp_controller/geo/estimated_location/tests/tests.py:                    "estimated_location_enabled",
openwisp_controller/geo/estimated_location/tests/tests.py-                    context_manager.exception.message_dict,
openwisp_controller/geo/estimated_location/tests/tests.py-                )
--
openwisp_controller/geo/estimated_location/tests/tests.py-        with self.subTest("Estimated location can be enabled when WHOIS is configured"):
openwisp_controller/geo/estimated_location/tests/tests.py-            geo_settings = org.geo_settings
openwisp_controller/geo/estimated_location/tests/tests.py:            geo_settings.estimated_location_enabled = True
openwisp_controller/geo/estimated_location/tests/tests.py-            # Should not raise
openwisp_controller/geo/estimated_location/tests/tests.py-            geo_settings.full_clean()
--
openwisp_controller/geo/estimated_location/tests/tests.py-    def test_estimated_location_field(self):
openwisp_controller/geo/estimated_location/tests/tests.py-        org = self._get_org()
openwisp_controller/geo/estimated_location/tests/tests.py:        # Disable estimated_location_enabled via OrganizationGeoSettings
openwisp_controller/geo/estimated_location/tests/tests.py:        org.geo_settings.estimated_location_enabled = False
openwisp_controller/geo/estimated_location/tests/tests.py-        org.geo_settings.save()
openwisp_controller/geo/estimated_location/tests/tests.py-        with self.assertRaises(ValidationError) as context_manager:
--
openwisp_controller/geo/estimated_location/tests/tests.py-            self.assertNotContains(response, "field-is_estimated")
openwisp_controller/geo/estimated_location/tests/tests.py-
openwisp_controller/geo/estimated_location/tests/tests.py:        org.geo_settings.estimated_location_enabled = False
openwisp_controller/geo/estimated_location/tests/tests.py-        org.geo_settings.save()
openwisp_controller/geo/estimated_location/tests/tests.py-        org.geo_settings.refresh_from_db()
--
openwisp_controller/geo/estimated_location/tests/tests.py-        org.config_settings.whois_enabled = True
openwisp_controller/geo/estimated_location/tests/tests.py-        org.config_settings.save()
openwisp_controller/geo/estimated_location/tests/tests.py:        org.geo_settings.estimated_location_enabled = True
openwisp_controller/geo/estimated_location/tests/tests.py-        org.geo_settings.save()
openwisp_controller/geo/estimated_location/tests/tests.py-
--
openwisp_controller/geo/estimated_location/tests/tests.py-                mocked_set.assert_not_called()
openwisp_controller/geo/estimated_location/tests/tests.py-                # The cache `get` is called twice, once for `whois_enabled` and
openwisp_controller/geo/estimated_location/tests/tests.py:                # once for `estimated_location_enabled`
openwisp_controller/geo/estimated_location/tests/tests.py-                mocked_get.assert_called()
openwisp_controller/geo/estimated_location/tests/tests.py-                mocked_estimated_location_task.assert_called()
--
openwisp_controller/geo/estimated_location/tests/tests.py-        new_time = timezone.now() - timedelta(days=threshold)
openwisp_controller/geo/estimated_location/tests/tests.py-        org = self._get_org()
openwisp_controller/geo/estimated_location/tests/tests.py:        org.geo_settings.estimated_location_enabled = False
openwisp_controller/geo/estimated_location/tests/tests.py-        org.geo_settings.save()
openwisp_controller/geo/estimated_location/tests/tests.py-        device = self._create_device(last_ip="172.217.22.10")
--
openwisp_controller/geo/estimated_location/tests/tests.py-            # Accessing devicelocation to verify it doesn't exist (raises if not)
openwisp_controller/geo/estimated_location/tests/tests.py-            device.devicelocation
openwisp_controller/geo/estimated_location/tests/tests.py:        org.geo_settings.estimated_location_enabled = True
openwisp_controller/geo/estimated_location/tests/tests.py-        org.geo_settings.save()
openwisp_controller/geo/estimated_location/tests/tests.py-        whois_obj = device.whois_service.get_device_whois_info()
--
openwisp_controller/geo/estimated_location/tests/tests.py-            "Test Estimated Status unchanged if Estimated feature is disabled"
openwisp_controller/geo/estimated_location/tests/tests.py-        ):
openwisp_controller/geo/estimated_location/tests/tests.py:            org.geo_settings.estimated_location_enabled = False
openwisp_controller/geo/estimated_location/tests/tests.py-            org.geo_settings.save()
openwisp_controller/geo/estimated_location/tests/tests.py-            org.geo_settings.refresh_from_db()
--
openwisp_controller/geo/estimated_location/tests/tests.py-            " and desired fields not changed"
openwisp_controller/geo/estimated_location/tests/tests.py-        ):
openwisp_controller/geo/estimated_location/tests/tests.py:            org.geo_settings.estimated_location_enabled = True
openwisp_controller/geo/estimated_location/tests/tests.py-            org.geo_settings.save()
openwisp_controller/geo/estimated_location/tests/tests.py-            org.geo_settings.refresh_from_db()
--
openwisp_controller/geo/base/models.py-            (self._state.adding or estimated_status_changed)
openwisp_controller/geo/base/models.py-            and self.is_estimated
openwisp_controller/geo/base/models.py:            and not EstimatedLocationService.check_estimated_location_enabled(
openwisp_controller/geo/base/models.py-                self.organization_id
openwisp_controller/geo/base/models.py-            )
--
openwisp_controller/geo/base/models.py-        """
openwisp_controller/geo/base/models.py-        changed_fields = set()
openwisp_controller/geo/base/models.py:        if EstimatedLocationService.check_estimated_location_enabled(
openwisp_controller/geo/base/models.py-            self.organization_id
openwisp_controller/geo/base/models.py-        ):
--
openwisp_controller/geo/base/models.py-        on_delete=models.CASCADE,
openwisp_controller/geo/base/models.py-    )
openwisp_controller/geo/base/models.py:    estimated_location_enabled = FallbackBooleanChoiceField(
openwisp_controller/geo/base/models.py-        help_text=_("Whether the estimated location feature is enabled"),
openwisp_controller/geo/base/models.py-        fallback=geo_settings.ESTIMATED_LOCATION_ENABLED,
--
openwisp_controller/geo/base/models.py-
openwisp_controller/geo/base/models.py-    def clean(self):
openwisp_controller/geo/base/models.py:        if not config_settings.WHOIS_CONFIGURED and self.estimated_location_enabled:
openwisp_controller/geo/base/models.py-            raise ValidationError(
openwisp_controller/geo/base/models.py-                {
openwisp_controller/geo/base/models.py:                    "estimated_location_enabled": _(
openwisp_controller/geo/base/models.py-                        "WHOIS_GEOIP_ACCOUNT and WHOIS_GEOIP_KEY must be set "
openwisp_controller/geo/base/models.py-                        "before enabling Estimated Location feature."
--
openwisp_controller/geo/admin.py-        # do not show the is_estimated field on add pages
openwisp_controller/geo/admin.py-        # or if the estimated location feature is not enabled for the organization
openwisp_controller/geo/admin.py:        if not obj or not EstimatedLocationService.check_estimated_location_enabled(
openwisp_controller/geo/admin.py-            obj.organization_id
openwisp_controller/geo/admin.py-        ):
--
openwisp_controller/geo/admin.py-    def get_readonly_fields(self, request, obj=None):
openwisp_controller/geo/admin.py-        fields = super().get_readonly_fields(request, obj)
openwisp_controller/geo/admin.py:        if obj and EstimatedLocationService.check_estimated_location_enabled(
openwisp_controller/geo/admin.py-            obj.organization_id
openwisp_controller/geo/admin.py-        ):
--
openwisp_controller/geo/admin.py-        obj = self.get_object(request, object_id)
openwisp_controller/geo/admin.py-        org_id = obj.organization_id if obj else None
openwisp_controller/geo/admin.py:        estimated_enabled = EstimatedLocationService.check_estimated_location_enabled(
openwisp_controller/geo/admin.py-            org_id
openwisp_controller/geo/admin.py-        )
--
openwisp_controller/geo/admin.py-        fields = list(super().get_readonly_fields(request, obj))
openwisp_controller/geo/admin.py-        if (
openwisp_controller/geo/admin.py:            "estimated_location_enabled" in fields
openwisp_controller/geo/admin.py-            and not config_app_settings.WHOIS_CONFIGURED
openwisp_controller/geo/admin.py-        ):
openwisp_controller/geo/admin.py:            fields = ["estimated_location_enabled"] + fields
openwisp_controller/geo/admin.py-        return fields
openwisp_controller/geo/admin.py-

Add cross-app migration dependency AND implement data migration logic in geo 0006.

The removal of estimated_location_enabled from config settings must wait for the geo migration to complete, but there's a deeper issue: geo.0006_create_geo_settings_for_existing_orgs creates new OrganizationGeoSettings records without migrating existing estimated_location_enabled values from config settings. This causes data loss if any organizations had estimated_location_enabled=True.

Add a dependency on the geo data migration to ensure proper ordering:

Proposed change
     dependencies = [
         (
             "config",
             "0063_organizationconfigsettings_estimated_location_enabled_and_more",
         ),
+        (
+            "geo",
+            "0006_create_geo_settings_for_existing_orgs",
+        ),
     ]

Additionally, geo.0006_create_geo_settings_for_existing_orgs should include a RunPython operation to copy estimated_location_enabled values from OrganizationConfigSettings to the newly created OrganizationGeoSettings for each organization.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class Migration(migrations.Migration):
dependencies = [
(
"config",
"0063_organizationconfigsettings_estimated_location_enabled_and_more",
),
]
class Migration(migrations.Migration):
dependencies = [
(
"config",
"0063_organizationconfigsettings_estimated_location_enabled_and_more",
),
(
"geo",
"0006_create_geo_settings_for_existing_orgs",
),
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py`
around lines 6 - 13, Migration 0064 must depend on the geo data migration and
the geo migration 0006 must copy the field before config removes it: add ("geo",
"0006_create_geo_settings_for_existing_orgs") to the dependencies list in
Migration class in
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py,
and modify geo migration
geo/migrations/0006_create_geo_settings_for_existing_orgs.py to include a
RunPython operation that defines a forward function (e.g.,
copy_estimated_location_enabled) which uses apps.get_model to load
OrganizationConfigSettings and OrganizationGeoSettings, iterates organizations
(or queries existing config records), and copies the estimated_location_enabled
boolean into the newly created OrganizationGeoSettings records (creating or
updating as needed), plus a safe reverse/noop function; use only migration-safe
ORM access (no direct model imports) and include this RunPython in the migration
operations so the config removal does not lose data.


operations = [
migrations.RemoveField(
model_name="organizationconfigsettings",
name="estimated_location_enabled",
),
]
13 changes: 2 additions & 11 deletions openwisp_controller/config/settings.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import logging

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _

logger = logging.getLogger(__name__)

from ..settings import get_setting

def get_setting(option, default):
return getattr(settings, f"OPENWISP_CONTROLLER_{option}", default)
logger = logging.getLogger(__name__)


BACKENDS = get_setting(
Expand Down Expand Up @@ -85,9 +82,3 @@ def get_setting(option, default):
raise ImproperlyConfigured(
"OPENWISP_CONTROLLER_WHOIS_REFRESH_THRESHOLD_DAYS must be a positive integer"
)
ESTIMATED_LOCATION_ENABLED = get_setting("ESTIMATED_LOCATION_ENABLED", False)
if ESTIMATED_LOCATION_ENABLED and not WHOIS_ENABLED:
raise ImproperlyConfigured(
"OPENWISP_CONTROLLER_WHOIS_ENABLED must be set to True before "
"setting OPENWISP_CONTROLLER_ESTIMATED_LOCATION_ENABLED to True."
)
8 changes: 8 additions & 0 deletions openwisp_controller/config/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,11 @@
vpn_server_modified.__doc__ = """
providing arguments: ['instance']
"""
whois_fetched = Signal()
whois_fetched.__doc__ = """
Providing arguments: ['whois', 'updated_fields', 'device']
"""
whois_lookup_skipped = Signal()
whois_lookup_skipped.__doc__ = """
Providing arguments: ['device']
"""
2 changes: 0 additions & 2 deletions openwisp_controller/config/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@
Device = load_model("config", "Device")
Config = load_model("config", "Config")
DeviceGroup = load_model("config", "DeviceGroup")
DeviceLocation = load_model("geo", "DeviceLocation")
Location = load_model("geo", "Location")
OrganizationUser = load_model("openwisp_users", "OrganizationUser")


Expand Down
Loading
Loading