Skip to content

[chores:fix] Separated geo.estimated_location from config.whois #1224#1315

Merged
nemesifier merged 16 commits intomasterfrom
issues/1224-separate-geo-logic-from-config
Apr 7, 2026
Merged

[chores:fix] Separated geo.estimated_location from config.whois #1224#1315
nemesifier merged 16 commits intomasterfrom
issues/1224-separate-geo-logic-from-config

Conversation

@pandafy
Copy link
Copy Markdown
Member

@pandafy pandafy commented Mar 23, 2026

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #1224

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR moves estimated-location responsibilities from the config app into geo. It adds an OrganizationGeoSettings model, migrations, admin inline, serializer, API view and URL; introduces EstimatedLocationService (caching, task enqueueing, persistence, cache invalidation); relocates estimated-location creation/update logic, tasks, handlers and utils to geo; emits two new WHOIS signals (whois_fetched, whois_lookup_skipped) and connects geo receivers on app ready; removes estimated-location fields/logic from config; and updates tests and the sample app to use the new geo settings.

Sequence Diagram(s)

sequenceDiagram
    participant WHOIS_Task as WHOIS Task
    participant EstService as EstimatedLocationService
    participant Cache as Cache Layer
    participant DB as Database
    participant Location as Location Model
    participant Celery as Celery Queue

    WHOIS_Task->>EstService: trigger_estimated_location_task(device_pk, ip)
    EstService->>Cache: get(cache_key(org_id))
    alt Cache hit
        Cache-->>EstService: org_geo_settings
    else Cache miss
        EstService->>DB: OrganizationGeoSettings.objects.get(organization_id=org_id)
        DB-->>EstService: org_geo_settings or DoesNotExist
        EstService->>Cache: set(cache_key, org_geo_settings|sentinel, timeout)
    end
    EstService->>EstService: evaluate estimated_location_enabled
    alt Enabled
        EstService->>Celery: send_task("whois_estimated_location_task", args)
        Celery->>Location: execute task -> create/update estimated Location (transactional save)
        Location->>DB: save
        DB-->>Location: saved instance
        EstService-->>WHOIS_Task: send notification (created/updated)
    else Disabled
        EstService-->>WHOIS_Task: no-op / skip
    end
Loading
sequenceDiagram
    participant WHOIS_Service as WHOIS processing
    participant Signals as whois_fetched / whois_lookup_skipped
    participant Handlers as geo estimated_location handlers
    participant EstService as EstimatedLocationService
    participant Celery as Celery Queue
    participant Device as Device Model

    WHOIS_Service->>Signals: emit whois_fetched(whois, updated_fields, device)
    Signals-->>Handlers: whois_fetched_handler(...)
    Handlers->>EstService: check_estimated_location_enabled(device.organization_id)
    alt Enabled
        Handlers->>EstService: EstimatedLocationService(Device).trigger_estimated_location_task(ip)
        EstService->>Celery: send_task("whois_estimated_location_task", args)
    else Disabled
        Handlers-->>WHOIS_Service: do nothing
    end

    Note over WHOIS_Service,Signals: whois_lookup_skipped triggers similar handler path
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • nemesifier
  • devkapilbansal
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is minimal and incomplete, missing key sections required by the template. Add a detailed description of changes in the 'Description of Changes' section, and include any relevant screenshots if applicable to demonstrate the implementation.
Title check ⚠️ Warning The PR title uses an incorrect type prefix format. It specifies '[chores:fix]' which mixes two type categories instead of using a single valid type (fix, change, feature, qa, ci, chores, or docs). Use a single type prefix from the allowed list. For example: '[fix] Separated geo.estimated_location from config.whois #1224' or '[chores] Separated geo.estimated_location from config.whois #1224'.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #1224: creates OrganizationGeoSettings model with estimated_location_enabled field [#1224], adds GeoSettingsInline to organization admin [#1224], implements REST API endpoint for geo settings [#1224], moves estimated-location logic to geo.estimated_location service [#1224], implements signal handlers in geo.apps [#1224], creates data migrations [#1224], and adds comprehensive tests [#1224].
Out of Scope Changes check ✅ Passed All changes are in-scope: geo-logic separation (config/whois, config/base), new geo models and admin (geo/admin, geo/models), new API endpoints (geo/api), signal setup (geo/apps), documentation updates (docs/), and supporting test infrastructure align with issue #1224 objectives.
Bug Fixes ✅ Passed This PR is a refactoring effort for architectural separation, not a user-facing bug fix. The migration fix is developer-facing, not user-facing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issues/1224-separate-geo-logic-from-config

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/user/rest-api.rst`:
- Around line 814-823: The docs list only PUT for the OrganizationGeoSettings
endpoint but the view OrganizationGeoSettingsView inherits RetrieveUpdateAPIView
(which allows PATCH), so either update the docs to include PATCH for
/api/v1/controller/organization/{organization_pk}/geo-settings/ OR restrict the
view to remove PATCH by adding http_method_names = ['get', 'put', 'head',
'options'] on OrganizationGeoSettingsView; locate the view (extends
RetrieveUpdateAPIView) to apply the change and ensure the docs and view methods
remain consistent.

In
`@openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py`:
- Around line 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.

In `@openwisp_controller/geo/admin.py`:
- Around line 212-222: The readonly-fields logic in
GeoSettingsInline.get_readonly_fields is inverted: instead of checking whether
"estimated_location_enabled" is already present, check whether WHOIS is not
configured and then ensure "estimated_location_enabled" is included; update
GeoSettingsInline.get_readonly_fields to test
config_app_settings.WHOIS_CONFIGURED (negated) and, if True, prepend or insert
"estimated_location_enabled" into the fields list (avoiding duplicates) for
OrganizationGeoSettings.

In `@openwisp_controller/geo/estimated_location/handlers.py`:
- Around line 39-56: The whois_info_post_save_handler uses
Device.objects.get(last_ip=instance.ip_address) which can raise
MultipleObjectsReturned; replace that lookup with
Device.objects.filter(last_ip=instance.ip_address).first() and treat a None
result the same way as the previous DoesNotExist case (return early). Update
references in the handler (Device, device, and the subsequent
EstimatedLocationService initialization/usage) so they only run when device is
not None.

In `@openwisp_controller/geo/estimated_location/service.py`:
- Around line 78-97: The update branch for estimated locations sets attrs on
current_location and calls current_location.save(update_fields=update_fields,
_set_estimated=True) without validation; mirror the create path by running
current_location.full_clean() (after setting attributes and before entering
transaction.atomic or before save) to validate the instance and raise/handle
ValidationError as appropriate, so replace the direct save with a validated save
in the block that builds update_fields for current_location and still call
send_whois_task_notification afterwards.
- Around line 31-35: Remove the redundant WHOIS_CONFIGURED check from the
is_estimated_location_enabled property: it should simply return the result of
check_estimated_location_enabled(self.device.organization_id) because
check_estimated_location_enabled already validates
config_app_settings.WHOIS_CONFIGURED; update the is_estimated_location_enabled
property to call and return check_estimated_location_enabled(...) directly and
remove the outer if-block.

In `@openwisp_controller/geo/estimated_location/tests/utils.py`:
- Around line 21-24: The test helper assumes org.geo_settings exists and will
raise RelatedObjectDoesNotExist for pre-existing organizations; update the logic
in _get_org() / the test setup to defensively fetch-or-create the related
OrganizationGeoSettings (use the OrganizationGeoSettings.get_or_create pattern
for the organization) before setting org.geo_settings.estimated_location_enabled
= True and calling save(), so the related object is guaranteed to exist when
toggling estimated_location_enabled.

In
`@openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py`:
- Around line 8-17: The migration function create_geo_settings_for_existing_orgs
currently iterates over Organization.objects.all(), which can load all
Organization rows into memory; change the iteration to use
Organization.objects.all().iterator() (or Organization.objects.iterator()) when
looping so each Organization is fetched one at a time, keeping the call to
OrganizationGeoSettings.objects.get_or_create(organization_id=org.pk) the same
and preserving behavior for Organization and OrganizationGeoSettings.

In `@openwisp_controller/geo/settings.py`:
- Around line 1-8: Remove the unused logger definition: delete the top-level
"logger = logging.getLogger(__name__)" and the "import logging" statement since
neither is used in this module; if you prefer to log configuration validation,
instead call logging.getLogger(...) locally where needed (e.g., inside functions
that use get_setting or config_settings) rather than keeping an unused
module-level logger.

In `@openwisp_controller/geo/tests/test_api.py`:
- Around line 1647-1659: The subtest "Cannot update without change permission"
incorrectly uses fake_uuid (a non-existent organization) and the logged-in
admin_user already has change permission, so it will return 404 not 403; either
remove this subtest entirely or relocate and rewrite it to target a real
organization's URL (use the existing url variable or the
test_organization_geo_settings_update test) and ensure the actor lacks change
permission (revoke change permission from admin_user or authenticate a user
without change rights) before calling client.put/client.patch so the response is
403 as intended.

In `@tests/openwisp2/sample_geo/models.py`:
- Around line 34-36: OrganizationGeoSettings currently inherits only from
AbstractOrganizationGeoSettings while other test swappable models include a
DetailsModel mixin to verify extensibility; modify the class declaration to mix
in DetailsModel (e.g., class OrganizationGeoSettings(DetailsModel,
AbstractOrganizationGeoSettings):) keep the nested Meta class as class
Meta(AbstractOrganizationGeoSettings.Meta): abstract = False, and ensure
DetailsModel is imported into this module so migrations/tests can detect the
extra custom field.
- Line 1: Remove the unused import of the swapper module at the top of the test
concrete model file: delete the line importing "swapper" since this concrete
test model does not use swapper.swappable_setting or any swapper symbols; verify
there are no remaining references to "swapper" in functions/classes in this file
(e.g., model class definitions) and run tests/lint to ensure no import is
required.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ed2367de-a081-4e33-855f-551ab7e0629f

📥 Commits

Reviewing files that changed from the base of the PR and between 45b24b6 and 5e762ae.

📒 Files selected for processing (32)
  • docs/developer/extending.rst
  • docs/user/rest-api.rst
  • openwisp_controller/config/admin.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/base/whois.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/api/serializers.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/base/models.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/mixins.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/utils.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/migrations/__init__.py
  • openwisp_controller/geo/models.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/utils.py
  • openwisp_controller/settings.py
  • tests/openwisp2/sample_geo/models.py
  • tests/openwisp2/settings.py
💤 Files with no reviewable changes (4)
  • openwisp_controller/config/tests/test_api.py
  • openwisp_controller/config/base/multitenancy.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/base/whois.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/geo/utils.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/mixins.py
  • tests/openwisp2/settings.py
  • openwisp_controller/geo/api/serializers.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/config/settings.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/settings.py
  • openwisp_controller/geo/models.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/utils.py
  • openwisp_controller/geo/migrations/__init__.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/base/models.py
  • openwisp_controller/geo/estimated_location/service.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/geo/utils.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/mixins.py
  • tests/openwisp2/settings.py
  • openwisp_controller/geo/api/serializers.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/config/settings.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/settings.py
  • openwisp_controller/geo/models.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/utils.py
  • openwisp_controller/geo/migrations/__init__.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/base/models.py
  • openwisp_controller/geo/estimated_location/service.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/geo/utils.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/mixins.py
  • tests/openwisp2/settings.py
  • openwisp_controller/geo/api/serializers.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/config/settings.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/settings.py
  • openwisp_controller/geo/models.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/utils.py
  • openwisp_controller/geo/migrations/__init__.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/base/models.py
  • openwisp_controller/geo/estimated_location/service.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/geo/utils.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/mixins.py
  • tests/openwisp2/settings.py
  • openwisp_controller/geo/api/serializers.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/config/settings.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/settings.py
  • openwisp_controller/geo/models.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/utils.py
  • openwisp_controller/geo/migrations/__init__.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/base/models.py
  • openwisp_controller/geo/estimated_location/service.py
🧠 Learnings (6)
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/geo/utils.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/mixins.py
  • openwisp_controller/geo/api/serializers.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/settings.py
  • openwisp_controller/geo/models.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/utils.py
  • openwisp_controller/geo/migrations/__init__.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/base/models.py
  • openwisp_controller/geo/estimated_location/service.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/geo/utils.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/mixins.py
  • openwisp_controller/geo/api/serializers.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/settings.py
  • openwisp_controller/geo/models.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/utils.py
  • openwisp_controller/geo/migrations/__init__.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/base/models.py
  • openwisp_controller/geo/estimated_location/service.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/geo/utils.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/mixins.py
  • tests/openwisp2/settings.py
  • openwisp_controller/geo/api/serializers.py
  • openwisp_controller/geo/api/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/config/settings.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/settings.py
  • openwisp_controller/geo/models.py
  • openwisp_controller/config/admin.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/utils.py
  • openwisp_controller/geo/migrations/__init__.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/base/models.py
  • openwisp_controller/geo/estimated_location/service.py
📚 Learning: 2026-01-15T14:06:53.460Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/base/models.py:63-93
Timestamp: 2026-01-15T14:06:53.460Z
Learning: In openwisp_controller/geo/base/models.py, the BaseLocation.save() method protects the is_estimated field from manual changes when the estimated location feature is disabled by reverting it to its initial database value using the elif branch. This is intentional behavior to ensure the field can only be modified when the feature is properly configured.

Applied to files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/mixins.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/settings.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/utils.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/base/models.py
  • openwisp_controller/geo/estimated_location/service.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • docs/developer/extending.rst
  • tests/openwisp2/settings.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/geo/models.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/base/models.py
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/whois/service.py
🧬 Code graph analysis (20)
openwisp_controller/geo/estimated_location/tests/utils.py (4)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (34-36)
openwisp_controller/config/whois/tests/utils.py (2)
  • CreateWHOISMixin (14-40)
  • setUp (36-40)
openwisp_controller/geo/base/models.py (1)
  • save (73-110)
openwisp_controller/geo/estimated_location/mixins.py (2)
openwisp_controller/geo/estimated_location/service.py (2)
  • EstimatedLocationService (12-98)
  • check_estimated_location_enabled (17-29)
openwisp_controller/geo/base/models.py (1)
  • organization_id (160-161)
openwisp_controller/geo/api/serializers.py (4)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (34-36)
openwisp_controller/geo/admin.py (1)
  • Meta (35-37)
openwisp_controller/geo/api/views.py (3)
  • Meta (53-55)
  • Meta (70-71)
  • Meta (100-102)
openwisp_controller/geo/api/views.py (4)
openwisp_controller/geo/api/serializers.py (1)
  • OrganizationGeoSettingsSerializer (26-30)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
openwisp_controller/mixins.py (1)
  • ProtectedAPIMixin (34-35)
openwisp_controller/geo/base/models.py (1)
  • organization_id (160-161)
openwisp_controller/geo/estimated_location/tasks.py (2)
openwisp_controller/geo/estimated_location/service.py (2)
  • EstimatedLocationService (12-98)
  • _create_or_update_estimated_location (44-98)
openwisp_controller/geo/estimated_location/utils.py (1)
  • get_location_defaults_from_whois (52-68)
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py (2)
openwisp_controller/geo/migrations/0005_organizationgeosettings.py (1)
  • Migration (12-62)
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py (1)
  • Migration (20-36)
openwisp_controller/config/settings.py (1)
openwisp_controller/settings.py (1)
  • get_setting (4-5)
tests/openwisp2/sample_geo/models.py (2)
openwisp_controller/geo/base/models.py (1)
  • AbstractOrganizationGeoSettings (164-204)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
openwisp_controller/geo/models.py (1)
openwisp_controller/geo/base/models.py (4)
  • AbstractOrganizationGeoSettings (164-204)
  • BaseDeviceLocation (127-161)
  • BaseFloorPlan (113-124)
  • BaseLocation (22-110)
openwisp_controller/geo/settings.py (1)
openwisp_controller/settings.py (1)
  • get_setting (4-5)
openwisp_controller/geo/apps.py (2)
openwisp_controller/geo/estimated_location/handlers.py (2)
  • register_estimated_location_notification_types (11-36)
  • whois_info_post_save_handler (39-56)
openwisp_controller/geo/base/models.py (1)
  • organization_post_save_receiver (198-204)
openwisp_controller/geo/estimated_location/handlers.py (2)
openwisp_controller/geo/estimated_location/service.py (3)
  • EstimatedLocationService (12-98)
  • check_estimated_location_enabled (17-29)
  • trigger_estimated_location_task (37-42)
openwisp_controller/geo/base/models.py (2)
  • device (156-157)
  • organization_id (160-161)
openwisp_controller/geo/estimated_location/utils.py (1)
openwisp_controller/config/base/whois.py (2)
  • _location_name (138-152)
  • formatted_address (120-135)
openwisp_controller/geo/migrations/__init__.py (3)
openwisp_controller/config/migrations/__init__.py (1)
  • assign_permissions_to_groups (26-60)
openwisp_controller/connection/migrations/__init__.py (1)
  • assign_permissions_to_groups (6-35)
openwisp_controller/pki/migrations/__init__.py (1)
  • assign_permissions_to_groups (6-31)
openwisp_controller/geo/migrations/0005_organizationgeosettings.py (2)
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py (1)
  • Migration (6-20)
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py (1)
  • Migration (20-36)
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py (3)
openwisp_controller/geo/migrations/__init__.py (1)
  • assign_geo_settings_permissions_to_groups (36-39)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
openwisp_controller/geo/base/models.py (1)
  • organization_id (160-161)
openwisp_controller/geo/admin.py (2)
openwisp_controller/config/admin.py (1)
  • DeactivatedDeviceReadOnlyMixin (83-104)
openwisp_controller/geo/estimated_location/service.py (2)
  • EstimatedLocationService (12-98)
  • check_estimated_location_enabled (17-29)
openwisp_controller/geo/estimated_location/tests/tests.py (3)
openwisp_controller/geo/models.py (2)
  • OrganizationGeoSettings (29-32)
  • Location (11-14)
openwisp_controller/geo/tests/utils.py (1)
  • TestGeoMixin (9-68)
openwisp_controller/geo/base/models.py (2)
  • save (73-110)
  • device (156-157)
openwisp_controller/geo/base/models.py (1)
openwisp_controller/geo/estimated_location/service.py (2)
  • EstimatedLocationService (12-98)
  • check_estimated_location_enabled (17-29)
openwisp_controller/geo/estimated_location/service.py (3)
openwisp_controller/geo/base/models.py (3)
  • device (156-157)
  • organization_id (160-161)
  • save (73-110)
openwisp_controller/geo/models.py (3)
  • OrganizationGeoSettings (29-32)
  • Location (11-14)
  • DeviceLocation (23-26)
openwisp_controller/config/whois/utils.py (1)
  • send_whois_task_notification (38-51)
🔇 Additional comments (53)
openwisp_controller/geo/migrations/0005_organizationgeosettings.py (1)

19-61: LGTM!

The migration correctly creates the OrganizationGeoSettings model with appropriate field definitions:

  • UUID primary key with auto-generation
  • Nullable FallbackBooleanChoiceField for estimated_location_enabled with sensible defaults
  • OneToOne relationship to organization with proper cascade delete
  • Swappable configuration for extensibility
openwisp_controller/geo/utils.py (1)

49-53: LGTM!

The new geo-settings endpoint follows the established URL patterns in this file and aligns with the REST API design for organization-scoped resources.

tests/openwisp2/settings.py (1)

283-283: LGTM!

The swapper configuration for OrganizationGeoSettings is correctly placed with other GEO model settings and follows the established naming convention.

docs/developer/extending.rst (2)

353-353: LGTM!

The swapper configuration documentation is correctly updated to include the new GEO_ORGANIZATIONGEOSETTINGS_MODEL setting.


460-464: LGTM!

The monkey patching example is appropriately updated to show how to import GeoSettingsInline alongside other admin classes from the geo module.

openwisp_controller/geo/estimated_location/tasks.py (2)

7-10: LGTM!

The imports are correctly updated to use the new EstimatedLocationService from the geo module and the extracted get_location_defaults_from_whois utility function, aligning with the PR objective of separating geo logic from config.


85-90: LGTM!

The refactoring correctly delegates to EstimatedLocationService for location creation/update, and the log message is appropriately consolidated into a single f-string.

openwisp_controller/geo/api/views.py (1)

349-359: Clean organization-scoped settings view wiring.

The new view + route export are coherent with the serializer/model wiring and return a clear translated 404 for missing org settings.

Also applies to: 373-373

openwisp_controller/geo/estimated_location/mixins.py (1)

14-37: Service transition in serializer mixins looks correct.

Switching to EstimatedLocationService keeps output behavior intact while aligning ownership with the geo module refactor.

openwisp_controller/settings.py (1)

4-8: Good consolidation of settings lookup.

Centralizing the OPENWISP_CONTROLLER_* key resolution improves consistency and reduces duplicated setting helpers.

openwisp_controller/geo/api/serializers.py (1)

26-30: Serializer addition is aligned with the new endpoint contract.

Read-only protection for organization and id is correctly set for organization-scoped settings updates.

Also applies to: 167-168

openwisp_controller/config/admin.py (1)

1390-1396: Admin inline field cleanup matches the module split.

Removing estimated_location_enabled from config inline and retaining whois_enabled is consistent with the geo-settings separation.

openwisp_controller/geo/estimated_location/utils.py (1)

52-68: Utility extraction is clean and purpose-specific.

get_location_defaults_from_whois is a good separation point for estimated-location defaults and aligns with the refactor intent.

openwisp_controller/config/settings.py (1)

6-8: Shared settings accessor usage is a good cleanup.

Using the common get_setting helper improves consistency across app modules.

openwisp_controller/geo/settings.py (1)

10-18: LGTM!

The configuration validation logic correctly ensures that WHOIS must be enabled before enabling the estimated location feature. The ImproperlyConfigured exception with a clear message is the appropriate Django pattern for configuration errors at startup.

openwisp_controller/geo/models.py (2)

3-8: LGTM!

The imports are correctly updated to include AbstractOrganizationGeoSettings from the base models.


29-33: LGTM!

The new OrganizationGeoSettings model follows the established swappable model pattern used by the existing Location, FloorPlan, and DeviceLocation models. The Meta class correctly inherits from the abstract parent and configures swappability.

openwisp_controller/geo/apps.py (3)

17-21: LGTM!

The imports are correctly updated to include config_app_settings and the new whois_info_post_save_handler from the estimated location handlers module.


29-35: LGTM!

The __setmodels__ method is correctly extended to load the additional models needed for signal handling and organization geo settings management.


46-57: LGTM!

The connect_receivers() method properly:

  • Uses dispatch_uid to prevent duplicate signal connections
  • Conditionally registers the WHOIS handler only when WHOIS_CONFIGURED is true
  • Cleanly separates signal wiring from the main ready() method
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py (1)

20-36: LGTM!

The migration structure is correct with proper dependencies on the model migration and openwisp_users. Using RunPython.noop for reverse operations is acceptable for additive data migrations.

openwisp_controller/config/whois/service.py (2)

15-16: LGTM!

The import cleanup correctly removes dependencies that were related to the estimated location functionality, which has been moved to the geo module.


207-220: LGTM!

The process_ip_data_and_location() method is correctly simplified to only handle WHOIS lookup triggering. The estimated location logic has been properly migrated to the geo module where it's triggered via signal handlers on WHOISInfo save.

openwisp_controller/geo/admin.py (3)

16-25: LGTM!

The imports are correctly updated to bring in the necessary components for the new geo settings functionality and the refactored estimated location service.


130-134: LGTM!

The switch from WHOISService.check_estimated_location_enabled to EstimatedLocationService.check_estimated_location_enabled correctly aligns with the refactoring that moved estimated location logic to the geo module.


225-228: LGTM!

The inline is correctly inserted after OrganizationLimitsInline in the OrganizationAdmin.inlines list.

openwisp_controller/geo/migrations/__init__.py (2)

6-25: LGTM!

The refactoring to extract _assign_permissions_to_groups as a shared helper follows DRY principles and maintains consistency with similar permission assignment patterns in other modules (pki, connection, config).


28-39: LGTM!

The delegation pattern cleanly separates the original geo model permissions from the new geo settings permissions while reusing the same underlying logic.

openwisp_controller/geo/base/models.py (5)

60-62: LGTM!

The migration from WHOISService.check_estimated_location_enabled() to EstimatedLocationService.check_estimated_location_enabled() correctly aligns with the PR objective to separate geo-specific logic from the config module.


88-90: LGTM!

Consistent with the clean() method update, the save() method now uses the new EstimatedLocationService for checking estimated location enablement.


164-175: LGTM!

Well-designed model with appropriate field choices:

  • OneToOneField ensures one settings record per organization
  • FallbackBooleanChoiceField with geo_settings.ESTIMATED_LOCATION_ENABLED as fallback provides sensible defaults
  • Proper i18n with _() for user-facing strings

185-195: LGTM!

The clean() validation correctly enforces that WHOIS must be configured before enabling the estimated location feature, matching the behavior previously in OrganizationConfigSettings.


197-204: LGTM!

The organization_post_save_receiver correctly uses get_or_create for idempotent settings creation when an organization is created. This pattern safely handles edge cases where the settings might already exist.

openwisp_controller/geo/tests/test_api.py (6)

1359-1366: LGTM!

Clean helper method for adding model permissions. Uses ContentType correctly to fetch permissions by codename pattern.


1368-1449: LGTM!

Comprehensive test coverage for retrieving organization geo settings:

  • Unauthenticated access returns 401
  • Missing permissions returns 403
  • With view permission returns 200 with expected fields
  • Cross-organization access returns 404
  • Superuser can access any organization

1450-1542: LGTM!

Thorough update operation tests covering:

  • PUT and PATCH operations
  • Organization field in PUT is correctly ignored
  • Cross-organization update attempts return 404
  • WHOIS configuration validation returns 400
  • Superuser can update any organization

1544-1581: LGTM!

Good multi-tenancy tests validating organization-scoped access:

  • User cannot access organizations they don't manage
  • User can access organizations they do manage
  • User managing multiple organizations can access both

1583-1612: LGTM!

Proper user access level tests covering:

  • Regular organization member without permissions gets 403
  • Organization admin with view permission gets 200
  • Superuser bypasses permission checks

1511-1521: 🧹 Nitpick | 🔵 Trivial

Test assertion may not verify intended behavior.

When WHOIS_CONFIGURED is patched to False, the PUT request with estimated_location_enabled=True returns 400 as expected. However, line 1520-1521 asserts that org1_geo_settings.estimated_location_enabled is still True after the failed request.

This assertion is checking that the value wasn't changed by the failed request, but since the previous successful PATCH on line 1477-1484 set it to True, this just confirms the value didn't change on validation failure - which is correct but could use a clarifying comment.

⛔ Skipped due to learnings
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/base/models.py:63-93
Timestamp: 2026-01-15T14:06:53.460Z
Learning: In openwisp_controller/geo/base/models.py, the BaseLocation.save() method protects the is_estimated field from manual changes when the estimated location feature is disabled by reverting it to its initial database value using the elif branch. This is intentional behavior to ensure the field can only be modified when the feature is properly configured.
openwisp_controller/geo/estimated_location/tests/tests.py (11)

40-42: LGTM!

Updated test class inheritance to use the new TestEstimatedLocationMixin and TestGeoMixin, properly reflecting the refactored module structure.


48-63: LGTM!

Tests correctly updated to validate the new OrganizationGeoSettings.estimated_location_enabled field instead of the old OrganizationConfigSettings field.


76-78: LGTM!

Admin form field name correctly updated from config_settings-0-estimated_location_enabled to geo_settings-0-estimated_location_enabled.


96-115: LGTM!

New dedicated test method for OrganizationGeoSettings validation provides clear coverage:

  • Validates that enabling estimated location requires WHOIS configuration
  • Confirms estimated location can be enabled when WHOIS is configured

121-123: LGTM!

Test correctly updated to disable estimated location via the new OrganizationGeoSettings model instead of the old config settings.


155-157: LGTM!

Test setup properly uses org.geo_settings for toggling estimated location feature.


234-235: LGTM!

Correctly updated to use geo_settings for enabling estimated location in test setup.


238-243: LGTM!

Context manager syntax properly updated to use parenthesized form for multiple mock.patch calls.


622-629: LGTM!

Test correctly toggles estimated location feature via OrganizationGeoSettings.


801-816: LGTM!

Test properly uses geo_settings for toggling estimated location feature during status change tests.


867-868: LGTM!

assertNumQueries count updated from previous value to 6, reflecting the changed query paths after model refactoring.

openwisp_controller/geo/estimated_location/service.py (3)

16-29: LGTM!

Well-designed static method with proper fallback logic:

  • Returns False early if org_id is missing or WHOIS not configured
  • Falls back to global ESTIMATED_LOCATION_ENABLED setting when per-org settings don't exist
  • Uses load_model to avoid import-time circular dependencies

37-42: LGTM!

Task dispatch method correctly uses settings.CELERY_APP.send_task() with appropriate kwargs for async processing.


44-77: LGTM!

Well-structured creation logic:

  • Properly handles case when device has no location
  • Uses transaction.atomic() for data integrity
  • Calls full_clean() before save for validation
  • Uses _set_estimated=True to prevent is_estimated from being cleared
  • Sends appropriate notification after successful creation

Comment thread docs/user/rest-api.rst
Comment on lines +6 to +13
class Migration(migrations.Migration):

dependencies = [
(
"config",
"0063_organizationconfigsettings_estimated_location_enabled_and_more",
),
]
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.

Comment thread openwisp_controller/geo/estimated_location/handlers.py Outdated
Comment thread openwisp_controller/geo/estimated_location/service.py
Comment thread openwisp_controller/geo/estimated_location/service.py
Comment thread openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py Outdated
Comment thread openwisp_controller/geo/settings.py Outdated
Comment thread openwisp_controller/geo/tests/test_api.py
Comment thread tests/openwisp2/sample_geo/models.py Outdated
Comment thread tests/openwisp2/sample_geo/models.py Outdated
@github-project-automation github-project-automation bot moved this from To do (general) to In progress in OpenWISP Contributor's Board Mar 23, 2026
@openwisp-companion
Copy link
Copy Markdown

Flake8 F401 Unused Import

Hello @pandafy,
(Analysis for commit 5e762ae)

The CI build failed due to a flake8 error. Specifically, the F401 error indicates an imported module that is not used.

Failure:

  • ./openwisp_controller/tests/openwisp2/models.py:1:1: F401 'swapper' imported but unused

Fix:
Remove the unused swapper import from the file ./openwisp_controller/tests/openwisp2/models.py.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

♻️ Duplicate comments (1)
openwisp_controller/geo/estimated_location/tests/utils.py (1)

16-25: ⚠️ Potential issue | 🟡 Minor

Create geo settings defensively in test setup.

_get_org() can still return an organization without a related OrganizationGeoSettings, so org.geo_settings may raise RelatedObjectDoesNotExist here. Fetch or create the row before toggling estimated_location_enabled.

🧪 Proposed fix
         org = self._get_org()
+        geo_settings, _ = OrganizationGeoSettings.objects.get_or_create(
+            organization=org
+        )
         OrganizationConfigSettings.objects.create(
             organization=org,
             whois_enabled=True,
         )
-        # OrganizationGeoSettings is auto-created by signal on Organization
-        # creation. Access the related object and update
-        # estimated_location_enabled to enable the feature for tests.
-        org.geo_settings.estimated_location_enabled = True
-        org.geo_settings.save()
+        geo_settings.estimated_location_enabled = True
+        geo_settings.save(update_fields=["estimated_location_enabled"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/geo/estimated_location/tests/utils.py` around lines 16 -
25, The test setup assumes org.geo_settings exists but may raise
RelatedObjectDoesNotExist; change the setup in the utils where _get_org() is
used to ensure OrganizationGeoSettings exists before toggling
estimated_location_enabled by using a get_or_create (or fetch-or-create) for the
OrganizationGeoSettings tied to the org, then set
estimated_location_enabled=True and save; keep the OrganizationConfigSettings
creation as-is and reference the org via _get_org(), OrganizationConfigSettings,
and OrganizationGeoSettings/ org.geo_settings to locate the affected code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/user/rest-api.rst`:
- Around line 814-834: Split the combined PUT/PATCH section into two
subsections: keep a "Update Organization Geographic Settings" subsection
containing only the PUT method and its curl example, and add a new "Partially
Update Organization Geographic Settings" subsection containing only the PATCH
method and its curl example; retain the endpoint path
/api/v1/controller/organization/{organization_pk}/geo-settings/ and explicitly
state in the PUT subsection that it represents a full replacement while the
PATCH subsection allows partial updates, and ensure each subsection has its own
code-blocks for the HTTP verb and a distinct curl invocation (one using PUT, one
using PATCH) so the docs match the established pattern used elsewhere.

In `@openwisp_controller/config/whois/tasks.py`:
- Around line 89-96: The on_commit lambda currently calls whois_fetched.send
which re-raises receiver exceptions; change it to call whois_fetched.send_robust
with the same kwargs (sender=WHOISInfo, whois=whois_obj,
updated_fields=update_fields, device=device) and iterate over the returned
(receiver, result) tuples to log any exceptions (result instances of Exception)
via the existing logger instead of letting them propagate; update the
transaction.on_commit callback that currently references whois_fetched.send to
use send_robust and explicit exception-logging.

In `@openwisp_controller/geo/admin.py`:
- Around line 212-224: The OrganizationGeoSettings inline currently allows the
singleton row to be deleted via the admin delete checkbox; update the inline to
prevent deletion by adding a class-level can_delete = False (and to be safe
enforce a single instance by setting max_num = 1 or overriding get_max_num to
return 1) on GeoSettingsInline (and apply the same change to the other inline
block around the second occurrence), so admins can only edit fields (like
estimated_location_enabled) and cannot remove the singleton
OrganizationGeoSettings row.

In `@openwisp_controller/geo/apps.py`:
- Around line 66-71: The whois_fetched signal handler (whois_fetched_handler) no
longer covers cases where a device is created or its last_ip changes to an IP
with an already-fresh WHOISInfo row because
WHOISService.process_ip_data_and_location() fallback was removed; add a geo-side
hook that triggers the same estimated-location logic when a Device's last_ip is
created/updated (e.g., connect a receiver to the Device model
post_save/post_update or the serializer/handler that changes last_ip and call
the existing whois_fetched_handler or the internal function that applies
estimated location), or restore the prior fallback call inside
WHOISService.process_ip_data_and_location() to invoke the estimated-location
routine when a fresh WHOISInfo exists; reference whois_fetched,
whois_fetched_handler, and WHOISService.process_ip_data_and_location() to locate
and wire the fix.

In `@openwisp_controller/geo/estimated_location/service.py`:
- Around line 37-44: The code path that handles a missing
OrganizationGeoSettings currently returns early without populating the cache,
causing repeated DB lookups; modify the block that catches
OrganizationGeoSettings.DoesNotExist (where org_settings is fetched) to cache
the no-row fallback (e.g. cache.set(cache_key,
geo_settings.estimated_location_enabled) or a sentinel/False value) before
returning so subsequent calls hit the cache; update the logic around
org_settings, cache_key and OrganizationGeoSettings lookup in service.py to
store the fallback value and then return it.
- Around line 64-68: The send_task call in trigger_estimated_location_task
currently runs inside the WHOIS signal handler and can raise if the broker is
down; wrap current_app.send_task(...) in a try/except that catches broad
exceptions, log the exception at error level (e.g., using
current_app.logger.error or the module logger) with context including
self.device.pk and ip_address, and do not re-raise so the device save proceeds
even if publishing fails.

In `@openwisp_controller/geo/estimated_location/tests/tests.py`:
- Around line 350-355: The test is patching send_whois_task_notification in the
wrong module so the real function runs; update the mocks to patch
openwisp_controller.geo.estimated_location.service.send_whois_task_notification
(the symbol imported and called by
EstimatedLocationService._create_or_update_estimated_location) instead of
config.whois.utils or geo.estimated_location.tasks, and apply the same change to
the other occurrence around lines 749-756 so the service-level notification call
is intercepted and prevents real side effects.

In
`@openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py`:
- Around line 13-14: Replace hard-coded apps.get_model("geo",
"OrganizationGeoSettings") (and any apps.get_model usage for the swappable
OrganizationConfigSettings) with the project helper get_swapped_model to resolve
swapped models at runtime: import get_swapped_model from
openwisp_controller.migrations and call get_swapped_model(apps, "geo",
"OrganizationGeoSettings") (and get_swapped_model(apps, "config",
"OrganizationConfigSettings") where applicable) so the migration targets the
installed/swapped models instead of historical defaults; update the copy loop
that uses getattr(..., None) to operate on the swapped models returned by
get_swapped_model.

In `@openwisp_controller/geo/settings.py`:
- Line 8: Remove the redundant inline comment "# Validate that WHOIS is enabled
if estimated location is enabled" in openwisp_controller/geo/settings.py; the
conditional that enforces WHOIS when estimated location is enabled is already
self-descriptive, so delete that comment line to satisfy the guideline against
unnecessary comments.
- Around line 9-13: The startup validation currently checks
config_settings.WHOIS_ENABLED when ESTIMATED_LOCATION_ENABLED is true; change
this to check config_settings.WHOIS_CONFIGURED instead so the validation ensures
WHOIS_GEOIP_ACCOUNT/WHOIS_GEOIP_KEY are present. Update the condition in the
block that references ESTIMATED_LOCATION_ENABLED and the raised
ImproperlyConfigured check from WHOIS_ENABLED to WHOIS_CONFIGURED (and adjust
the exception message to mention WHOIS credentials/WHOIS_CONFIGURED) so the
startup validation matches the rest of the codebase.

---

Duplicate comments:
In `@openwisp_controller/geo/estimated_location/tests/utils.py`:
- Around line 16-25: The test setup assumes org.geo_settings exists but may
raise RelatedObjectDoesNotExist; change the setup in the utils where _get_org()
is used to ensure OrganizationGeoSettings exists before toggling
estimated_location_enabled by using a get_or_create (or fetch-or-create) for the
OrganizationGeoSettings tied to the org, then set
estimated_location_enabled=True and save; keep the OrganizationConfigSettings
creation as-is and reference the org via _get_org(), OrganizationConfigSettings,
and OrganizationGeoSettings/ org.geo_settings to locate the affected code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6fa30623-e4df-4400-9300-e23ae417b40b

📥 Commits

Reviewing files that changed from the base of the PR and between 5e762ae and 82021c7.

📒 Files selected for processing (16)
  • docs/developer/utils.rst
  • docs/user/rest-api.rst
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/settings.py
  • tests/openwisp2/sample_geo/models.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tasks.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/config/whois/service.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tasks.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/config/whois/service.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tasks.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/config/whois/service.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tasks.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/config/whois/service.py
🧠 Learnings (7)
📓 Common learnings
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/base/models.py:63-93
Timestamp: 2026-01-15T14:06:53.460Z
Learning: In openwisp_controller/geo/base/models.py, the BaseLocation.save() method protects the is_estimated field from manual changes when the estimated location feature is disabled by reverting it to its initial database value using the elif branch. This is intentional behavior to ensure the field can only be modified when the feature is properly configured.
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tasks.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/config/whois/service.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/config/whois/service.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tasks.py
  • tests/openwisp2/sample_geo/models.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/config/whois/service.py
📚 Learning: 2026-01-15T14:06:53.460Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/base/models.py:63-93
Timestamp: 2026-01-15T14:06:53.460Z
Learning: In openwisp_controller/geo/base/models.py, the BaseLocation.save() method protects the is_estimated field from manual changes when the estimated location feature is disabled by reverting it to its initial database value using the elif branch. This is intentional behavior to ensure the field can only be modified when the feature is properly configured.

Applied to files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/config/whois/service.py
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/config/whois/service.py
🧬 Code graph analysis (12)
openwisp_controller/config/whois/tasks.py (3)
openwisp_controller/config/models.py (1)
  • WHOISInfo (117-124)
tests/openwisp2/sample_config/models.py (1)
  • WHOISInfo (117-123)
openwisp_controller/geo/base/models.py (1)
  • device (156-157)
tests/openwisp2/sample_geo/models.py (4)
openwisp_controller/geo/base/models.py (1)
  • AbstractOrganizationGeoSettings (164-204)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
tests/openwisp2/sample_connection/models.py (1)
  • DetailsModel (10-14)
tests/openwisp2/sample_pki/models.py (1)
  • DetailsModel (6-10)
openwisp_controller/geo/estimated_location/tests/utils.py (4)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (33-35)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
openwisp_controller/geo/estimated_location/tests/tests.py (2)
  • setUp (203-210)
  • setUp (840-843)
openwisp_controller/geo/base/models.py (1)
  • save (73-110)
openwisp_controller/config/whois/tests/tests.py (1)
openwisp_controller/config/whois/tasks.py (1)
  • fetch_whois_details (62-101)
openwisp_controller/geo/estimated_location/handlers.py (2)
openwisp_controller/geo/estimated_location/service.py (3)
  • EstimatedLocationService (16-118)
  • check_estimated_location_enabled (21-45)
  • trigger_estimated_location_task (64-68)
openwisp_controller/geo/base/models.py (2)
  • device (156-157)
  • organization_id (160-161)
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py (3)
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py (1)
  • Migration (42-61)
openwisp_controller/geo/migrations/0005_organizationgeosettings.py (1)
  • Migration (12-62)
openwisp_controller/config/base/config.py (1)
  • name (132-139)
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py (6)
openwisp_controller/geo/migrations/__init__.py (1)
  • assign_geo_settings_permissions_to_groups (36-39)
tests/openwisp2/sample_users/models.py (1)
  • Organization (32-36)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (33-35)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py (1)
  • Migration (6-20)
openwisp_controller/geo/migrations/0005_organizationgeosettings.py (1)
  • Migration (12-62)
openwisp_controller/geo/apps.py (4)
openwisp_controller/geo/estimated_location/handlers.py (2)
  • register_estimated_location_notification_types (11-36)
  • whois_fetched_handler (39-62)
openwisp_controller/geo/estimated_location/service.py (2)
  • EstimatedLocationService (16-118)
  • invalidate_org_settings_cache (53-58)
openwisp_controller/geo/base/models.py (1)
  • organization_post_save_receiver (198-204)
openwisp_controller/config/base/whois.py (1)
  • invalidate_org_settings_cache (111-117)
openwisp_controller/geo/admin.py (5)
openwisp_controller/admin.py (1)
  • MultitenantAdminMixin (24-29)
openwisp_controller/config/admin.py (2)
  • DeactivatedDeviceReadOnlyMixin (83-104)
  • OrganizationLimitsInline (1370-1374)
openwisp_controller/geo/estimated_location/service.py (2)
  • EstimatedLocationService (16-118)
  • check_estimated_location_enabled (21-45)
openwisp_controller/geo/models.py (4)
  • DeviceLocation (23-26)
  • FloorPlan (17-20)
  • Location (11-14)
  • OrganizationGeoSettings (29-32)
openwisp_controller/geo/base/models.py (1)
  • organization_id (160-161)
openwisp_controller/geo/estimated_location/tests/tests.py (5)
openwisp_controller/geo/estimated_location/handlers.py (1)
  • register_estimated_location_notification_types (11-36)
openwisp_controller/geo/estimated_location/service.py (3)
  • EstimatedLocationService (16-118)
  • get_cache_key (48-50)
  • check_estimated_location_enabled (21-45)
openwisp_controller/geo/models.py (2)
  • OrganizationGeoSettings (29-32)
  • Location (11-14)
openwisp_controller/geo/estimated_location/tests/utils.py (1)
  • TestEstimatedLocationMixin (12-40)
openwisp_controller/geo/base/models.py (2)
  • save (73-110)
  • device (156-157)
openwisp_controller/geo/estimated_location/service.py (6)
openwisp_controller/config/whois/utils.py (1)
  • send_whois_task_notification (38-51)
openwisp_controller/geo/base/models.py (3)
  • device (156-157)
  • organization_id (160-161)
  • save (73-110)
openwisp_controller/geo/models.py (3)
  • OrganizationGeoSettings (29-32)
  • Location (11-14)
  • DeviceLocation (23-26)
openwisp_controller/config/whois/service.py (1)
  • get_cache_key (42-46)
openwisp_controller/config/base/whois.py (1)
  • invalidate_org_settings_cache (111-117)
openwisp_controller/config/base/config.py (1)
  • full_clean (558-562)
openwisp_controller/geo/settings.py (1)
openwisp_controller/settings.py (1)
  • get_setting (4-5)

Comment thread docs/user/rest-api.rst
Comment on lines +89 to +96
transaction.on_commit(
lambda: whois_fetched.send(
sender=WHOISInfo,
whois=whois_obj,
updated_fields=update_fields,
device=device,
)
)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 24, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the target file
fd -t f "tasks.py" | grep whois

Repository: openwisp/openwisp-controller

Length of output: 113


🏁 Script executed:

# Check the specific lines in question
if [ -f "openwisp_controller/config/whois/tasks.py" ]; then
  wc -l openwisp_controller/config/whois/tasks.py
fi

Repository: openwisp/openwisp-controller

Length of output: 117


🏁 Script executed:

# Read the relevant section with context
if [ -f "openwisp_controller/config/whois/tasks.py" ]; then
  sed -n '75,110p' openwisp_controller/config/whois/tasks.py
fi

Repository: openwisp/openwisp-controller

Length of output: 1548


🏁 Script executed:

# Check the imports at the top of the file
if [ -f "openwisp_controller/config/whois/tasks.py" ]; then
  head -30 openwisp_controller/config/whois/tasks.py
fi

Repository: openwisp/openwisp-controller

Length of output: 976


🏁 Script executed:

# Search for whois_fetched signal definition
rg "whois_fetched\s*=" --type py -B 2 -A 2

Repository: openwisp/openwisp-controller

Length of output: 411


🏁 Script executed:

# Search for receivers registered to whois_fetched
rg "whois_fetched\s*\.\s*(connect|disconnect)" --type py -B 2 -A 2

Repository: openwisp/openwisp-controller

Length of output: 500


🏁 Script executed:

# Find and examine the whois_fetched_handler
rg "whois_fetched_handler" --type py -B 5 -A 20

Repository: openwisp/openwisp-controller

Length of output: 5996


🏁 Script executed:

# Also check if there are other receivers connected to whois_fetched
rg "whois_fetched\.connect\(" --type py -B 2 -A 2

Repository: openwisp/openwisp-controller

Length of output: 500


🌐 Web query:

Django Signal send() vs send_robust() exception handling behavior

💡 Result:

  • Signal.send(): does not catch exceptions from receivers. If any receiver raises, the exception propagates and the dispatch loop stops (so later receivers may not run). Returns [(receiver, response), ...] only if no receiver raises. [1], [2]

  • Signal.send_robust(): wraps each receiver call in try/except Exception. If a receiver raises a subclass of Exception, Django continues to call remaining receivers and returns the exception object in that receiver’s slot: [(receiver, response_or_exception), ...]. In newer Django versions it also logs the error. It does not catch BaseException subclasses like KeyboardInterrupt/SystemExit. [1], [2], [3]

Sources:
[1] Django “Signals” docs (exception-handling difference) (docs.djangoproject.com)
[2] Django module source showing send() behavior (propagate/stop) (docs.djangoproject.com)
[3] Django module source showing send_robust() behavior (catch/log/continue) (docs.djangoproject.com)

Citations:


Use robust signal dispatch after commit.

Signal.send() will re-raise receiver exceptions after the WHOIS row has already been committed, so a buggy receiver can mark this task as failed and drive the generic WHOIS error path even though the lookup succeeded. Switch this callback to send_robust() and log any returned exceptions instead of letting them unwind the task.

🔧 Suggested hardening
-        transaction.on_commit(
-            lambda: whois_fetched.send(
-                sender=WHOISInfo,
-                whois=whois_obj,
-                updated_fields=update_fields,
-                device=device,
-            )
-        )
+        def _emit_whois_fetched():
+            for receiver, response in whois_fetched.send_robust(
+                sender=WHOISInfo,
+                whois=whois_obj,
+                updated_fields=update_fields,
+                device=device,
+            ):
+                if isinstance(response, Exception):
+                    logger.error(
+                        "whois_fetched receiver %r failed for %s: %s",
+                        receiver,
+                        new_ip_address,
+                        response,
+                    )
+
+        transaction.on_commit(_emit_whois_fetched)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/whois/tasks.py` around lines 89 - 96, The
on_commit lambda currently calls whois_fetched.send which re-raises receiver
exceptions; change it to call whois_fetched.send_robust with the same kwargs
(sender=WHOISInfo, whois=whois_obj, updated_fields=update_fields, device=device)
and iterate over the returned (receiver, result) tuples to log any exceptions
(result instances of Exception) via the existing logger instead of letting them
propagate; update the transaction.on_commit callback that currently references
whois_fetched.send to use send_robust and explicit exception-logging.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@coderabbitai are you hallucinating? Can you re-review this comment and explain the problem properly?

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.

@pandafy, I re-reviewed the comment carefully. The concern is technically valid, not a hallucination — let me explain it more clearly.

Here is the exact failure path:

  1. The WHOIS data is successfully written to the DB inside the with transaction.atomic(): block.
  2. After the transaction commits, transaction.on_commit fires the lambda that calls whois_fetched.send(...).
  3. Signal.send() does not catch exceptions from receivers. If any receiver raises, the exception propagates upward into the Celery task execution context (since on_commit runs synchronously within the task after the atomic block exits).
  4. This causes fetch_whois_details to be treated as a failed task, which triggers WHOISCeleryRetryTask.on_failure().
  5. on_failure() then sends a whois_device_error notification — a false positive alert, because the WHOIS fetch itself actually succeeded.

The current receiver (whois_fetched_handler) is simple and unlikely to raise in normal operation. But the concern is about defensive correctness: as more receivers are potentially connected to whois_fetched in the future, any one of them failing would produce a misleading failure notification for a task that completed successfully.

That said, I acknowledge this is a relatively low-risk concern given the current receiver implementation. If you feel this is acceptable for now, we could defer it or track it as a follow-up. Would you like me to open an issue for it?


🧠 Learnings used
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Comment thread openwisp_controller/geo/admin.py
Comment thread openwisp_controller/geo/apps.py Outdated
Comment thread openwisp_controller/geo/estimated_location/service.py
Comment thread openwisp_controller/geo/estimated_location/service.py Outdated
Comment thread openwisp_controller/geo/estimated_location/tests/tests.py
Comment thread openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py Outdated
Comment thread openwisp_controller/geo/settings.py
Comment thread openwisp_controller/geo/settings.py Outdated
@openwisp-companion
Copy link
Copy Markdown

Test Failures Detected

Hello @pandafy,
(Analysis for commit 82021c7)

  1. Test Failure: The CI pipeline failed due to test failures. Specifically, the runtests.py script encountered errors when executing the tests. The traceback indicates that the django.core.checks.urls module failed to resolve an error handler, which is a critical part of Django's URL routing and error handling. This suggests a misconfiguration or an issue within the URL patterns or error handling setup in the project.
  • Fix: Review the openwisp_controller/urls.py file and ensure that all URL patterns are correctly defined and that custom error handlers (if any) are properly configured and accessible. Check for any missing or incorrectly referenced URL patterns that might lead to a resolver.resolve_error_handler failure.
  1. Test Failure: The logs indicate a significant number of slow tests detected (532). While not a direct failure, this points to potential performance issues or inefficient test configurations that could lead to timeouts or longer CI runs in the future.
  • Fix: Investigate the tests marked as slow (indicated by [31;1m in the logs) and optimize them. This might involve improving test logic, reducing redundant operations, or using more efficient testing strategies. Consider marking genuinely slow but necessary tests with a tag("slow") to allow for selective running.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openwisp_controller/config/whois/service.py (1)

215-224: ⚠️ Potential issue | 🟠 Major

Emit whois_lookup_skipped only after the save transaction commits.

The fetch branch right above correctly uses transaction.on_commit(...) to defer the celery task, but this branch sends the signal synchronously. openwisp_controller/geo/estimated_location/handlers.py enqueues the estimated-location task immediately from that receiver, creating a race where the handler executes before the transaction fully commits. The pattern already exists in openwisp_controller/config/whois/tasks.py, where whois_fetched is deferred with transaction.on_commit() for the same reason.

Suggested fix
-        elif self.is_whois_enabled and self.is_valid_public_ip_address(new_ip):
-            # Emit signal when lookup is skipped so receivers can react
-            whois_lookup_skipped.send(sender=self.__class__, device=self.device)
+        elif self.is_whois_enabled and self.is_valid_public_ip_address(new_ip):
+            transaction.on_commit(
+                lambda: whois_lookup_skipped.send(
+                    sender=self.__class__,
+                    device=self.device,
+                )
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/whois/service.py` around lines 215 - 224, The
whois_lookup_skipped signal is being sent synchronously in the elif branch
(conditions: self.is_whois_enabled and self.is_valid_public_ip_address(new_ip)),
which can race with DB commit; wrap the signal send in transaction.on_commit so
it fires only after commit (mirror the fetch branch). Specifically, replace the
direct whois_lookup_skipped.send(sender=self.__class__, device=self.device) call
with a transaction.on_commit(lambda:
whois_lookup_skipped.send(sender=self.__class__, device=self.device)) so the
device and sender are captured and the signal is deferred until after the save
transaction completes.
♻️ Duplicate comments (1)
openwisp_controller/geo/tests/test_api.py (1)

1647-1659: ⚠️ Potential issue | 🟡 Minor

This subtest never reaches the intended 403 branch.

url still points to fake_uuid, so both requests stay on the same 404 path covered above. The test also grants change permission on Lines 1620-1622, so it does not represent "without change permission" either. Remove this block or move it to a real-organization URL with a user lacking change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/geo/tests/test_api.py` around lines 1647 - 1659, The
failing subtest uses the same fake_uuid url and also mistakenly grants change
permission earlier, so it never exercises the "403 without change" branch;
update the test by either removing this subTest block or changing it to request
a real-organization URL (replace the fake_uuid url with the actual org resource
URL used elsewhere in the test) and ensure the test user does NOT have the
'change' permission (remove the earlier grant of change permission or explicitly
revoke it) before making the PUT/PATCH calls to assert 403; look for the
variables/methods named url, the subTest block text "Cannot update without
change permission", and the permission-granting lines that give 'change' to
adjust accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_controller/geo/settings.py`:
- Around line 13-16: The error text is misleading: the check uses
WHOIS_CONFIGURED but the message points to OPENWISP_CONTROLLER_WHOIS_ENABLED;
update the ImproperlyConfigured message (the raise in the block that validates
OPENWISP_CONTROLLER_ESTIMATED_LOCATION_ENABLED) to reference the actual
requirement (WHOIS_CONFIGURED) and/or list the required credentials
(WHOIS_GEOIP_ACCOUNT and WHOIS_GEOIP_KEY) so users know they must configure
those values rather than merely toggling OPENWISP_CONTROLLER_WHOIS_ENABLED.

In `@openwisp_controller/geo/tests/test_api.py`:
- Around line 1511-1521: The assertion after the failing PUT is wrong: instead
of asserting org1_geo_settings.estimated_location_enabled is True, refresh the
org1_geo_settings (call org1_geo_settings.refresh_from_db()) and assert it
remains the previous value (False) to confirm the failed update left the row
unchanged; update the assertion referencing org1_geo_settings and its
estimated_location_enabled attribute accordingly.

---

Outside diff comments:
In `@openwisp_controller/config/whois/service.py`:
- Around line 215-224: The whois_lookup_skipped signal is being sent
synchronously in the elif branch (conditions: self.is_whois_enabled and
self.is_valid_public_ip_address(new_ip)), which can race with DB commit; wrap
the signal send in transaction.on_commit so it fires only after commit (mirror
the fetch branch). Specifically, replace the direct
whois_lookup_skipped.send(sender=self.__class__, device=self.device) call with a
transaction.on_commit(lambda: whois_lookup_skipped.send(sender=self.__class__,
device=self.device)) so the device and sender are captured and the signal is
deferred until after the save transaction completes.

---

Duplicate comments:
In `@openwisp_controller/geo/tests/test_api.py`:
- Around line 1647-1659: The failing subtest uses the same fake_uuid url and
also mistakenly grants change permission earlier, so it never exercises the "403
without change" branch; update the test by either removing this subTest block or
changing it to request a real-organization URL (replace the fake_uuid url with
the actual org resource URL used elsewhere in the test) and ensure the test user
does NOT have the 'change' permission (remove the earlier grant of change
permission or explicitly revoke it) before making the PUT/PATCH calls to assert
403; look for the variables/methods named url, the subTest block text "Cannot
update without change permission", and the permission-granting lines that give
'change' to adjust accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 136103a7-cea4-4cbb-9c8b-b9cc6c4c6878

📥 Commits

Reviewing files that changed from the base of the PR and between 82021c7 and 58bb5db.

📒 Files selected for processing (15)
  • docs/developer/utils.rst
  • docs/user/rest-api.rst
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • tests/openwisp2/sample_geo/views.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • tests/openwisp2/sample_geo/views.py
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • tests/openwisp2/sample_geo/views.py
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • tests/openwisp2/sample_geo/views.py
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • tests/openwisp2/sample_geo/views.py
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
🧠 Learnings (9)
📓 Common learnings
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/base/models.py:63-93
Timestamp: 2026-01-15T14:06:53.460Z
Learning: In openwisp_controller/geo/base/models.py, the BaseLocation.save() method protects the is_estimated field from manual changes when the estimated location feature is disabled by reverting it to its initial database value using the elif branch. This is intentional behavior to ensure the field can only be modified when the feature is properly configured.
📚 Learning: 2026-01-15T14:06:53.460Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/base/models.py:63-93
Timestamp: 2026-01-15T14:06:53.460Z
Learning: In openwisp_controller/geo/base/models.py, the BaseLocation.save() method protects the is_estimated field from manual changes when the estimated location feature is disabled by reverting it to its initial database value using the elif branch. This is intentional behavior to ensure the field can only be modified when the feature is properly configured.

Applied to files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • tests/openwisp2/sample_geo/views.py
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • tests/openwisp2/sample_geo/views.py
  • openwisp_controller/config/signals.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/admin.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-03-14T19:34:33.773Z
Learnt from: CR
Repo: openwisp/openwisp-controller PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-03-14T19:34:33.773Z
Learning: Applies to **/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp} : Avoid unnecessary comments or docstrings for code that is already clear

Applied to files:

  • openwisp_controller/geo/settings.py
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-03-17T09:20:10.456Z
Learnt from: pandafy
Repo: openwisp/openwisp-controller PR: 1292
File: openwisp_controller/connection/tasks.py:27-31
Timestamp: 2026-03-17T09:20:10.456Z
Learning: In `openwisp_controller/connection/tasks.py`, the `update_config` Celery task only accepts one argument `device_id`, which is always passed as a string (via `str(device.pk)`) from the call site in `openwisp_controller/connection/apps.py`. Do not flag `str(device_id) in task["args"]` as unreliable due to typed args — the args are always strings.

Applied to files:

  • openwisp_controller/geo/estimated_location/service.py
🧬 Code graph analysis (12)
openwisp_controller/geo/estimated_location/tests/utils.py (4)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (33-35)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
openwisp_controller/config/whois/tests/utils.py (2)
  • CreateWHOISMixin (14-40)
  • setUp (36-40)
openwisp_controller/geo/base/models.py (1)
  • save (73-110)
tests/openwisp2/sample_geo/views.py (1)
openwisp_controller/geo/api/views.py (1)
  • OrganizationGeoSettingsView (349-358)
openwisp_controller/config/whois/tests/tests.py (5)
openwisp_controller/config/tests/test_selenium.py (1)
  • SeleniumTestMixin (26-50)
openwisp_controller/config/whois/handlers.py (1)
  • connect_whois_handlers (7-29)
openwisp_controller/config/whois/tests/utils.py (1)
  • _mocked_client_response (45-57)
openwisp_controller/config/whois/tasks.py (1)
  • fetch_whois_details (62-101)
openwisp_controller/config/whois/service.py (1)
  • process_ip_data_and_location (208-224)
openwisp_controller/geo/estimated_location/handlers.py (2)
openwisp_controller/geo/estimated_location/service.py (3)
  • EstimatedLocationService (16-127)
  • check_estimated_location_enabled (21-47)
  • trigger_estimated_location_task (66-78)
openwisp_controller/geo/base/models.py (2)
  • device (156-157)
  • organization_id (160-161)
openwisp_controller/geo/apps.py (5)
openwisp_controller/geo/estimated_location/handlers.py (3)
  • register_estimated_location_notification_types (11-36)
  • whois_fetched_handler (39-62)
  • whois_lookup_skipped_handler (65-75)
openwisp_controller/geo/estimated_location/service.py (2)
  • EstimatedLocationService (16-127)
  • invalidate_org_settings_cache (55-60)
openwisp_controller/config/apps.py (3)
  • ready (45-54)
  • register_dashboard_charts (342-458)
  • register_menu_groups (163-199)
openwisp_controller/geo/base/models.py (1)
  • organization_post_save_receiver (198-204)
openwisp_controller/config/base/whois.py (1)
  • invalidate_org_settings_cache (111-117)
openwisp_controller/geo/settings.py (1)
openwisp_controller/settings.py (1)
  • get_setting (4-5)
openwisp_controller/geo/admin.py (4)
openwisp_controller/geo/estimated_location/service.py (2)
  • EstimatedLocationService (16-127)
  • check_estimated_location_enabled (21-47)
tests/openwisp2/sample_geo/models.py (4)
  • DeviceLocation (28-30)
  • FloorPlan (23-25)
  • Location (18-20)
  • OrganizationGeoSettings (33-35)
openwisp_controller/geo/models.py (4)
  • DeviceLocation (23-26)
  • FloorPlan (17-20)
  • Location (11-14)
  • OrganizationGeoSettings (29-32)
openwisp_controller/geo/base/models.py (1)
  • organization_id (160-161)
openwisp_controller/config/whois/service.py (2)
openwisp_controller/config/whois/tasks.py (1)
  • fetch_whois_details (62-101)
openwisp_controller/geo/base/models.py (1)
  • device (156-157)
openwisp_controller/geo/tests/test_api.py (3)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (33-35)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
tests/openwisp2/sample_users/models.py (1)
  • OrganizationUser (39-43)
openwisp_controller/geo/estimated_location/service.py (3)
openwisp_controller/config/whois/utils.py (1)
  • send_whois_task_notification (38-51)
openwisp_controller/geo/base/models.py (3)
  • device (156-157)
  • organization_id (160-161)
  • save (73-110)
openwisp_controller/geo/models.py (3)
  • OrganizationGeoSettings (29-32)
  • Location (11-14)
  • DeviceLocation (23-26)
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py (7)
openwisp_controller/geo/migrations/__init__.py (1)
  • assign_geo_settings_permissions_to_groups (36-39)
openwisp_controller/migrations.py (1)
  • get_swapped_model (13-16)
tests/openwisp2/sample_users/models.py (1)
  • Organization (32-36)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (33-35)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py (1)
  • Migration (6-20)
openwisp_controller/geo/migrations/0005_organizationgeosettings.py (1)
  • Migration (12-62)
openwisp_controller/geo/estimated_location/tests/tests.py (5)
openwisp_controller/geo/estimated_location/service.py (3)
  • EstimatedLocationService (16-127)
  • get_cache_key (50-52)
  • check_estimated_location_enabled (21-47)
tests/openwisp2/sample_geo/models.py (2)
  • OrganizationGeoSettings (33-35)
  • Location (18-20)
openwisp_controller/geo/models.py (2)
  • OrganizationGeoSettings (29-32)
  • Location (11-14)
openwisp_controller/geo/estimated_location/tests/utils.py (1)
  • TestEstimatedLocationMixin (12-41)
openwisp_controller/geo/base/models.py (2)
  • save (73-110)
  • device (156-157)
🔇 Additional comments (7)
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py (1)

1-63: LGTM!

The migration correctly addresses previous review feedback:

  • Uses get_swapped_model() for resolving swappable OrganizationConfigSettings and OrganizationGeoSettings models
  • Uses iterator() for memory-efficient iteration over large datasets
  • Properly handles the data copy with getattr() fallback for field safety
openwisp_controller/geo/estimated_location/tests/utils.py (1)

16-26: LGTM!

The test setup correctly:

  • Uses get_or_create pattern to defensively ensure OrganizationGeoSettings exists (addressing the previous review concern about RelatedObjectDoesNotExist)
  • Separates WHOIS enablement (OrganizationConfigSettings) from estimated location enablement (OrganizationGeoSettings), aligning with the PR's refactoring goal
tests/openwisp2/sample_geo/views.py (1)

28-30: LGTM!

The new OrganizationGeoSettingsView follows the established pattern in this file for swappable API views—importing the base, creating an empty subclass for extensibility, and registering the as_view() callable.

Also applies to: 69-72, 82-82

docs/developer/utils.rst (1)

382-409: LGTM!

The signal documentation follows the established pattern in this file and clearly describes:

  • The signal paths
  • The arguments provided
  • When each signal is emitted

This aligns with the signal definitions in openwisp_controller/config/signals.py.

openwisp_controller/config/signals.py (1)

68-75: LGTM!

The new signals follow the established pattern in this module with proper __doc__ strings documenting the provided arguments.

docs/user/rest-api.rst (1)

804-850: LGTM!

The API documentation for Organization Geographic Settings is well-structured and follows the established patterns in this file. It properly documents:

  • GET endpoint for retrieving settings
  • PUT endpoint for full updates with a curl example
  • PATCH endpoint for partial updates with a curl example (addressing previous review feedback about separating methods)
openwisp_controller/config/whois/tests/tests.py (1)

454-473: LGTM!

The new signal emission tests are well-structured:

  • test_fetch_whois_emits_signal verifies whois_fetched is emitted after successful WHOIS lookup
  • test_whois_lookup_skipped_emits_signal verifies whois_lookup_skipped is emitted when lookup conditions aren't met (existing WHOISInfo for the IP)

Both tests properly use catch_signal and follow the established testing patterns in this file.

Comment thread openwisp_controller/geo/settings.py Outdated
Comment thread openwisp_controller/geo/tests/test_api.py Outdated
@openwisp-companion
Copy link
Copy Markdown

Code Style Failures in CI

Hello @pandafy,
(Analysis for commit 58bb5db)

There are several code style and formatting issues detected in the CI run:

  1. Import Sorting and Formatting: The isort check failed, indicating that imports are not correctly sorted or formatted in openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py.
  • Fix: Run openwisp-qa-format to automatically fix import order and formatting.
  1. Line Too Long (E501): The flake8 check reported a line that exceeds the maximum length in openwisp_controller/geo/estimated_location/tests/utils.py.
  • Fix: Manually shorten the line at openwisp_controller/geo/estimated_location/tests/utils.py:24:89 to comply with the 88-character limit.
  1. Unused Import (F401): The flake8 check found an imported module that is not used in openwisp_controller/geo/estimated_location/service.py.
  • Fix: Remove the unused import ..settings as geo_settings from openwisp_controller/geo/estimated_location/service.py.
  1. Black Formatting: The Black check failed, indicating formatting issues in multiple files.
  • Fix: Run openwisp-qa-format to automatically format the code according to Black's standards.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_controller/geo/estimated_location/tests/tests.py`:
- Around line 111-115: This subtest relies on WHOIS being enabled but never
ensures it; before calling geo_settings.full_clean() in the "Estimated location
can be enabled when WHOIS is configured" subTest, explicitly enable the WHOIS
flag (e.g., set config_app_settings.WHOIS_CONFIGURED = True or use
override/monkeypatch) so the validation path for
geo_settings.estimated_location_enabled passes; ensure you restore/reset the
flag after the subtest if needed.
- Around line 266-269: The patched cache.get in the second and third subtests is
left as a truthy MagicMock so the cache-hit branch runs; change the mocks for
"django.core.cache.cache.get" to return concrete values (e.g., return_value=None
for explicit misses, or return_value=<actual settings object with
.estimated_location_enabled set appropriately> for cache hits) and keep
cache.set mocked as before so the tests exercise the DB-fallback and
cached-settings paths; locate the patches around the context manager that mocks
"django.core.cache.cache.get" / "django.core.cache.cache.set" and instantiate or
reuse the real settings object used elsewhere in the test to supply to
mocked_get.return_value so .estimated_location_enabled is read from a real
object rather than a MagicMock.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a39e01e-7619-4eb5-91d2-ef92b8a78666

📥 Commits

Reviewing files that changed from the base of the PR and between 58bb5db and cc29d7c.

📒 Files selected for processing (4)
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/tests/utils.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
🧠 Learnings (8)
📓 Common learnings
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/base/models.py:63-93
Timestamp: 2026-01-15T14:06:53.460Z
Learning: In openwisp_controller/geo/base/models.py, the BaseLocation.save() method protects the is_estimated field from manual changes when the estimated location feature is disabled by reverting it to its initial database value using the elif branch. This is intentional behavior to ensure the field can only be modified when the feature is properly configured.
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-01-15T14:06:53.460Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/base/models.py:63-93
Timestamp: 2026-01-15T14:06:53.460Z
Learning: In openwisp_controller/geo/base/models.py, the BaseLocation.save() method protects the is_estimated field from manual changes when the estimated location feature is disabled by reverting it to its initial database value using the elif branch. This is intentional behavior to ensure the field can only be modified when the feature is properly configured.

Applied to files:

  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/tests/test_api.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-03-14T19:34:33.773Z
Learnt from: CR
Repo: openwisp/openwisp-controller PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-03-14T19:34:33.773Z
Learning: Applies to **/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp} : Avoid unnecessary comments or docstrings for code that is already clear

Applied to files:

  • openwisp_controller/geo/settings.py
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/geo/settings.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/geo/estimated_location/tests/tests.py
🧬 Code graph analysis (3)
openwisp_controller/geo/settings.py (1)
openwisp_controller/settings.py (1)
  • get_setting (4-5)
openwisp_controller/geo/tests/test_api.py (4)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (33-35)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
tests/openwisp2/sample_users/models.py (1)
  • OrganizationUser (39-43)
openwisp_controller/geo/api/serializers.py (2)
  • create (188-207)
  • create (345-356)
openwisp_controller/geo/estimated_location/tests/tests.py (4)
openwisp_controller/geo/estimated_location/handlers.py (1)
  • register_estimated_location_notification_types (11-36)
openwisp_controller/geo/estimated_location/service.py (3)
  • EstimatedLocationService (16-127)
  • get_cache_key (50-52)
  • check_estimated_location_enabled (21-47)
tests/openwisp2/sample_geo/models.py (2)
  • OrganizationGeoSettings (33-35)
  • Location (18-20)
openwisp_controller/geo/models.py (2)
  • OrganizationGeoSettings (29-32)
  • Location (11-14)
🔇 Additional comments (9)
openwisp_controller/geo/settings.py (1)

8-14: Use WHOIS_CONFIGURED instead of WHOIS_ENABLED and remove the redundant comment.

Two issues persist from prior review feedback:

  1. Line 8: The comment restates what the code already expresses clearly. Remove it per coding guidelines.

  2. Lines 10-13: Checking WHOIS_ENABLED is insufficient. If credentials (WHOIS_GEOIP_ACCOUNT/WHOIS_GEOIP_KEY) are missing, this validation passes at startup but the feature fails at runtime. Use WHOIS_CONFIGURED instead, which is the established gate throughout the codebase (e.g., geo/apps.py, geo/base/models.py).

🛠️ Proposed fix
 ESTIMATED_LOCATION_ENABLED = get_setting("ESTIMATED_LOCATION_ENABLED", False)
 
-# Validate that WHOIS is enabled if estimated location is enabled
 if ESTIMATED_LOCATION_ENABLED:
-    if not config_settings.WHOIS_ENABLED:
+    if not config_settings.WHOIS_CONFIGURED:
         raise ImproperlyConfigured(
-            "OPENWISP_CONTROLLER_WHOIS_ENABLED must be set to True before "
-            "setting OPENWISP_CONTROLLER_ESTIMATED_LOCATION_ENABLED to True."
+            "WHOIS must be configured (OPENWISP_CONTROLLER_WHOIS_GEOIP_ACCOUNT "
+            "and OPENWISP_CONTROLLER_WHOIS_GEOIP_KEY must be set) before "
+            "enabling OPENWISP_CONTROLLER_ESTIMATED_LOCATION_ENABLED."
         )
openwisp_controller/geo/tests/test_api.py (7)

1615-1669: LGTM!

The non-existent organization tests properly return 404 for unknown UUIDs. The "Cannot update without change permission" subtest correctly addresses the previous review concern by:

  1. Revoking the change permission
  2. Using a real organization URL (real_url)
  3. Asserting 403 (Forbidden) as expected

7-7: LGTM!

The new imports are necessary for the OrganizationGeoSettings API tests - ContentType for the permission helper and OrganizationGeoSettings model for the endpoint tests.

Also applies to: 31-31


1359-1366: LGTM!

The permission helper is well-structured and correctly uses Django's ContentType API to dynamically grant model-level permissions for test assertions.


1368-1450: LGTM!

Comprehensive test coverage for the retrieve endpoint, properly testing authentication (401), authorization (403), multi-tenancy isolation (404), and successful retrieval scenarios.


1451-1543: LGTM!

The update tests are comprehensive and the validation error test correctly asserts that the setting remains False (the previous state) after the failed PUT request. Good coverage of PUT, PATCH, payload sanitization, multi-tenancy, validation, and superuser scenarios.


1545-1582: LGTM!

Good multi-tenancy isolation tests ensuring users can only access organizations they manage.


1584-1613: LGTM!

Properly tests the access level hierarchy: regular members are denied, org admins with model permissions succeed, and superusers bypass permission checks.

openwisp_controller/geo/tests/utils.py (1)

22-25: The _get_org() method is available and correctly inherited from TestLociMixin.

The method is provided by the django_loci.tests.TestLociMixin parent class and is consistently used throughout the codebase in hundreds of test methods across the project. No action needed—the implementation is correct.

Comment thread openwisp_controller/geo/estimated_location/tests/tests.py
Comment thread openwisp_controller/geo/estimated_location/tests/tests.py
@openwisp-companion
Copy link
Copy Markdown

Code Style and Test Failures

Hello @pandafy,
(Analysis for commit cc29d7c)

There are multiple issues in your commit:

  1. Code Style (Imports): The imports in openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py are incorrectly sorted. Please run openwisp-qa-format to fix this.

  2. Code Style (Line Too Long): The line org_geo_settings, _ = OrganizationGeoSettings.objects.get_or_create( in openwisp_controller/geo/estimated_location/tests/utils.py is too long. Please manually fix this by breaking the line.

  3. Code Style (Unused Import): The import ..settings as geo_settings in openwisp_controller/geo/estimated_location/service.py is unused. Please remove it.

  4. Test Failure (Slow Tests): There are 581 slow tests detected. Please investigate and optimize them.

  5. Test Failure (Black): The Black formatter check failed. Please run openwisp-qa-format to fix this.

@openwisp-companion
Copy link
Copy Markdown

Flake8 and Test Failures

Hello @pandafy,
(Analysis for commit 5f34d78)

  1. Code Style/QA: A Flake8 error was detected:
  • F401 '..settings as geo_settings' imported but unused in openwisp_controller/geo/estimated_location/service.py:11:1.
    To fix this, remove the unused import.
  1. Test Failure: Several tests failed:
  • AssertionError: 4 != 0 : Response should not contain 'errors' in test_device_change_location_from_outdoor_to_indoor and test_remove_mobile.
  • AssertionError: 302 != 200 in test_readonly_indoor_location and test_readonly_indoor_object_location.
  • AssertionError: 400 != 200 in test_organization_geo_settings_update (PUT with organization field).
  • AssertionError: True != False in test_organization_geo_settings_update (Validation error when WHOIS not configured).
  • AssertionError: 0 != 1 in test_can_change_inline_org_owner (owner can edit) and test_can_change_inline_org_owner (superuser can edit).
  • AssertionError: Expected 'mock' to have been called. in test_fetch_whois_emits_signal.
  • AssertionError: 0 != 1 in test_estimated_location_notification (create and update).
  • AssertionError: Expected 'error' to be called once. Called 0 times. in test_estimated_location_notification (conflicting locations).
  • AssertionError: Expected 'trigger_estimated_location_task' to have been called. in multiple test_estimated_location_task_called tests.

These test failures indicate issues with how the application handles responses, status codes, permissions, or signal emissions in various scenarios. Review the test logic and the corresponding code to identify the root cause of these assertions failing. Specifically, check the expected return values, status codes, and signal calls in the failing tests.

@pandafy pandafy force-pushed the issues/1224-separate-geo-logic-from-config branch from 5f34d78 to 0a08bb1 Compare March 24, 2026 19:41
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
openwisp_controller/geo/estimated_location/tests/tests.py (1)

267-270: ⚠️ Potential issue | 🟡 Minor

Return real cache values in these two subtests.

mock.patch("django.core.cache.cache.get") without a return_value gives you a truthy MagicMock, so both flows take the cache-hit path and read .whois_enabled / .estimated_location_enabled from mocks. These assertions can pass without exercising either the DB fallback or the real cached-settings behavior.

♻️ Suggested fix
         with self.subTest(
             "Estimated location task called when last_ip is changed and is public"
         ):
             with (
-                mock.patch("django.core.cache.cache.get") as mocked_get,
+                mock.patch(
+                    "django.core.cache.cache.get",
+                    side_effect=lambda key, default=None: {
+                        f"organization_config_{org.pk}": org.config_settings,
+                        f"organization_geo_{org.pk}": org.geo_settings,
+                    }.get(key, default),
+                ) as mocked_get,
                 mock.patch("django.core.cache.cache.set") as mocked_set,
             ):
                 device.last_ip = "172.217.22.10"
                 device.save()
@@
         with self.subTest(
             "Estimated location task called when last_ip has related WhoIsInfo"
         ):
             with (
-                mock.patch("django.core.cache.cache.get") as mocked_get,
+                mock.patch(
+                    "django.core.cache.cache.get",
+                    side_effect=lambda key, default=None: {
+                        f"organization_config_{org.pk}": org.config_settings,
+                        f"organization_geo_{org.pk}": org.geo_settings,
+                    }.get(key, default),
+                ) as mocked_get,
                 mock.patch("django.core.cache.cache.set") as mocked_set,
             ):

Also applies to: 287-289

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/geo/estimated_location/tests/tests.py` around lines 267 -
270, The patch of django.core.cache.cache.get in the two subtests currently
returns a MagicMock (truthy) and forces the cache-hit branch; update the
mock.patch calls for "django.core.cache.cache.get" to provide an explicit
return_value (or side_effect) representing the real cached value you expect in
each subtest (e.g., None to force DB-fallback or a dict/boolean matching the
cached-settings shape like {'whois_enabled': True} or False), keep mocking
"django.core.cache.cache.set" as needed and assert set/get behavior accordingly
so the test actually exercises both cache-miss (DB fallback) and real
cached-settings paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py`:
- Around line 46-49: The migration's dependencies list is missing the config
migration that introduces OrganizationConfigSettings.estimated_location_enabled,
which copy_estimated_location_enabled() reads; update the dependencies variable
to include ("config",
"0063_organizationconfigsettings_estimated_location_enabled_and_more") so the
historical model state during RunPython has the source field available and the
migration runs reliably.

---

Duplicate comments:
In `@openwisp_controller/geo/estimated_location/tests/tests.py`:
- Around line 267-270: The patch of django.core.cache.cache.get in the two
subtests currently returns a MagicMock (truthy) and forces the cache-hit branch;
update the mock.patch calls for "django.core.cache.cache.get" to provide an
explicit return_value (or side_effect) representing the real cached value you
expect in each subtest (e.g., None to force DB-fallback or a dict/boolean
matching the cached-settings shape like {'whois_enabled': True} or False), keep
mocking "django.core.cache.cache.set" as needed and assert set/get behavior
accordingly so the test actually exercises both cache-miss (DB fallback) and
real cached-settings paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 93ccf516-7f34-4950-bc2e-23f5c3b9f0a4

📥 Commits

Reviewing files that changed from the base of the PR and between cc29d7c and 0a08bb1.

📒 Files selected for processing (6)
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/tests/mixins.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}

📄 CodeRabbit inference engine (Custom checks)

**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp}: Flag potential security vulnerabilities in code
Avoid unnecessary comments or docstrings for code that is already clear
Code formatting is compact and readable. Do not add excessive blank lines, especially inside function or method bodies
Flag unused or redundant code
Ensure variables, functions, classes, and files have descriptive and consistent names
New code must handle errors properly: log errors that cannot be resolved by the user with error level, log unusual conditions with warning level, log important background actions with info level, and provide user-facing messages for errors that the user can solve autonomously

Files:

  • openwisp_controller/tests/mixins.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sql}

📄 CodeRabbit inference engine (Custom checks)

Flag obvious performance regressions, such as heavy loops, repeated I/O, or unoptimized queries

Files:

  • openwisp_controller/tests/mixins.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
**/*.{js,ts,tsx,jsx,py,java,go,cs,rb,php,c,cpp,h,hpp,sh,bash,sql}

📄 CodeRabbit inference engine (Custom checks)

Cryptic or non-obvious code (regex, complex bash commands, or hard-to-read code) must include a concise comment explaining why it is needed and why the complexity is acceptable

Files:

  • openwisp_controller/tests/mixins.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
**/*.{py,html}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • openwisp_controller/tests/mixins.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
🧠 Learnings (7)
📚 Learning: 2026-01-12T22:27:48.342Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: tests/openwisp2/sample_config/migrations/0008_whoisinfo_organizationconfigsettings_whois_enabled.py:18-67
Timestamp: 2026-01-12T22:27:48.342Z
Learning: In tests/openwisp2/sample_config/models.py and corresponding test migrations, the WHOISInfo model intentionally includes an additional "details" field not present in the base AbstractWHOISInfo model. This is a testing pattern to verify that swappable models (CONFIG_WHOISINFO_MODEL) can be extended with custom fields without errors.

Applied to files:

  • openwisp_controller/tests/mixins.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-01-15T15:05:49.557Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/management/commands/clear_last_ip.py:38-42
Timestamp: 2026-01-15T15:05:49.557Z
Learning: In Django projects, when using select_related() to traverse relations (for example, select_related("organization__config_settings")), the traversed relation must not be deferred. If you also use .only() in the same query, include the relation name or FK field (e.g., "organization" or "organization_id") in the .only() list to avoid the error "Field X cannot be both deferred and traversed using select_related at the same time." Apply this guideline to Django code in openwisp_controller/config/management/commands/clear_last_ip.py and similar modules by ensuring any select_related with an accompanying only() includes the related field names to prevent deferred/traversed conflicts.

Applied to files:

  • openwisp_controller/tests/mixins.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-02-17T19:13:10.088Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/config/whois/commands.py:0-0
Timestamp: 2026-02-17T19:13:10.088Z
Learning: In reviews for the openwisp/openwisp-controller repository, do not propose changes based on Ruff warnings. The project does not use Ruff as its linter; ignore Ruff-related suggestions and follow the repository’s established linting and configuration rules. This guidance applies to all Python files under the openwisp_controller directory.

Applied to files:

  • openwisp_controller/tests/mixins.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-01-15T15:07:17.354Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/estimated_location/tests/tests.py:172-175
Timestamp: 2026-01-15T15:07:17.354Z
Learning: In this repository, flake8 enforces E501 (line too long) via setup.cfg (max-line-length = 88) while ruff ignores E501 via ruff.toml. Therefore, use '# noqa: E501' on lines that intentionally exceed 88 characters to satisfy flake8 without affecting ruff checks. This applies to Python files across the project (any .py) and is relevant for tests as well. Use sparingly and only where breaking lines is not feasible without hurting readability or functionality.

Applied to files:

  • openwisp_controller/tests/mixins.py
  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-01-15T14:06:53.460Z
Learnt from: DragnEmperor
Repo: openwisp/openwisp-controller PR: 1175
File: openwisp_controller/geo/base/models.py:63-93
Timestamp: 2026-01-15T14:06:53.460Z
Learning: In openwisp_controller/geo/base/models.py, the BaseLocation.save() method protects the is_estimated field from manual changes when the estimated location feature is disabled by reverting it to its initial database value using the elif branch. This is intentional behavior to ensure the field can only be modified when the feature is properly configured.

Applied to files:

  • openwisp_controller/geo/estimated_location/tests/utils.py
  • openwisp_controller/geo/apps.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
📚 Learning: 2026-03-17T09:20:10.456Z
Learnt from: pandafy
Repo: openwisp/openwisp-controller PR: 1292
File: openwisp_controller/connection/tasks.py:27-31
Timestamp: 2026-03-17T09:20:10.456Z
Learning: In `openwisp_controller/connection/tasks.py`, the `update_config` Celery task only accepts one argument `device_id`, which is always passed as a string (via `str(device.pk)`) from the call site in `openwisp_controller/connection/apps.py`. Do not flag `str(device_id) in task["args"]` as unreliable due to typed args — the args are always strings.

Applied to files:

  • openwisp_controller/geo/estimated_location/service.py
📚 Learning: 2026-02-05T20:06:01.589Z
Learnt from: stktyagi
Repo: openwisp/openwisp-controller PR: 1147
File: openwisp_controller/config/utils.py:222-222
Timestamp: 2026-02-05T20:06:01.589Z
Learning: In `openwisp_controller/config/utils.py`, the `send_api_task_notification` function is called from Celery background tasks. Do not use sleep() or blocking operations in this function as it blocks the Celery worker and prevents notifications from being sent.

Applied to files:

  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
🧬 Code graph analysis (5)
openwisp_controller/geo/estimated_location/tests/utils.py (4)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (33-35)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
openwisp_controller/config/whois/tests/utils.py (2)
  • CreateWHOISMixin (14-40)
  • setUp (36-40)
openwisp_controller/geo/base/models.py (1)
  • save (73-110)
openwisp_controller/geo/apps.py (4)
openwisp_controller/geo/estimated_location/handlers.py (3)
  • register_estimated_location_notification_types (11-36)
  • whois_fetched_handler (39-62)
  • whois_lookup_skipped_handler (65-75)
openwisp_controller/geo/estimated_location/service.py (2)
  • EstimatedLocationService (14-125)
  • invalidate_org_settings_cache (53-58)
openwisp_controller/geo/base/models.py (1)
  • organization_post_save_receiver (198-204)
openwisp_controller/config/base/whois.py (1)
  • invalidate_org_settings_cache (111-117)
openwisp_controller/geo/estimated_location/service.py (5)
openwisp_controller/config/whois/utils.py (1)
  • send_whois_task_notification (38-51)
openwisp_controller/geo/base/models.py (3)
  • device (156-157)
  • organization_id (160-161)
  • save (73-110)
openwisp_controller/geo/models.py (3)
  • OrganizationGeoSettings (29-32)
  • Location (11-14)
  • DeviceLocation (23-26)
openwisp_controller/config/whois/service.py (1)
  • get_cache_key (43-47)
openwisp_controller/config/base/whois.py (1)
  • invalidate_org_settings_cache (111-117)
openwisp_controller/geo/migrations/0006_create_geo_settings_for_existing_orgs.py (6)
openwisp_controller/migrations.py (1)
  • get_swapped_model (13-16)
openwisp_controller/geo/migrations/__init__.py (1)
  • assign_geo_settings_permissions_to_groups (36-39)
tests/openwisp2/sample_geo/models.py (1)
  • OrganizationGeoSettings (33-35)
openwisp_controller/geo/models.py (1)
  • OrganizationGeoSettings (29-32)
openwisp_controller/config/migrations/0064_remove_organizationconfigsettings_estimated_location_enabled_and_more.py (1)
  • Migration (6-20)
openwisp_controller/geo/migrations/0005_organizationgeosettings.py (1)
  • Migration (12-62)
openwisp_controller/geo/estimated_location/tests/tests.py (6)
openwisp_controller/geo/tests/utils.py (1)
  • TestGeoMixin (9-68)
openwisp_controller/geo/estimated_location/handlers.py (1)
  • register_estimated_location_notification_types (11-36)
openwisp_controller/geo/estimated_location/service.py (3)
  • EstimatedLocationService (14-125)
  • get_cache_key (48-50)
  • check_estimated_location_enabled (19-45)
openwisp_controller/geo/models.py (2)
  • OrganizationGeoSettings (29-32)
  • Location (11-14)
openwisp_controller/geo/estimated_location/tests/utils.py (1)
  • TestEstimatedLocationMixin (12-43)
openwisp_controller/tests/utils.py (1)
  • TestAdminMixin (9-18)
🔇 Additional comments (1)
openwisp_controller/tests/mixins.py (1)

14-18: LGTM!

The new geo_settings inline formset parameters follow the established pattern used by other inlines (config_settings, config_limits, notification_settings), and the comment maintains consistency with the existing code style. This correctly supports testing organization edit forms that include the new GeoSettingsInline.

@pandafy pandafy force-pushed the issues/1224-separate-geo-logic-from-config branch from 0a08bb1 to 9c36fab Compare March 24, 2026 19:55
@openwisp-companion
Copy link
Copy Markdown

Migrations Check Failed

Hello @pandafy,
(Analysis for commit 0a08bb1)

The CI build failed because of a migrations check failure. This indicates that there are unapplied model changes that need to be migrated.

Fix:
Run ./manage.py makemigrations to generate the necessary migration files and then commit them.

@openwisp-companion
Copy link
Copy Markdown

QA Failures in Migration File

Hello @pandafy,
(Analysis for commit 9c36fab)

  • Code Style/QA: The file tests/openwisp2/sample_geo/migrations/0005_organizationgeosettings.py has incorrectly sorted imports. Please run openwisp-qa-format to fix this.

@pandafy pandafy force-pushed the issues/1224-separate-geo-logic-from-config branch from 9c36fab to 249f28a Compare March 24, 2026 20:27
@openwisp-companion
Copy link
Copy Markdown

Test Failures in OpenWISP Controller CI

Hello @pandafy,
(Analysis for commit 249f28a)

There are multiple test failures in the CI run.

1. Test Failures:

  • RuntimeWarning: DateTimeField received a naive datetime:
    Several tests are producing RuntimeWarnings related to naive datetime objects being passed to DateTimeFields while timezone support is active. This indicates a potential issue with how datetime objects are being handled, possibly in test data or within the application logic.

Fix: Ensure all datetime objects used in tests and application logic are timezone-aware when timezone support is enabled in Django settings.

  • floorplan image not found:
    Multiple tests are failing because they cannot find floorplan images (floorplans/*.jpg). This suggests that either the test setup is missing these image files, or the paths to these files are incorrect in the test environment.

Fix: Ensure that the necessary floorplan image files are present in the expected locations within the test setup, or correct the paths in the tests if they are incorrect.

  • Detected tampering in clone template form:
    Several tests related to cloning templates are failing with various errors:
  • "Organization matching query does not exist."
  • "‘invalid_uuid’ is not a valid UUID."
  • "not authorized to operate on org_3."
  • "not authorized to clone a template and set it to shared."

These errors indicate issues with how organizations, UUIDs, and authorization are handled during the template cloning process in the tests.

Fix: Review the tests for template cloning to ensure that valid organization IDs and UUIDs are used, and that authorization checks are correctly implemented and passed.

  • Test failures flagged with [31;1m (red):
    A significant number of tests are failing, indicated by the red output in the logs. These failures are spread across various modules (admin, api, connection, geo, etc.) and cover a wide range of functionalities. Without specific error messages for each of these, it's difficult to pinpoint the exact cause. However, the presence of RuntimeWarnings and the "floorplan image not found" errors suggest that there might be underlying setup or data issues affecting multiple tests.

Fix:

  1. Address the RuntimeWarnings and "floorplan image not found" errors first, as they might be contributing to or causing these broader test failures.
  2. If the issues persist, systematically debug each failing test, paying close attention to the specific assertions and the code being tested. Look for any recent changes that might have introduced these failures.

2. Potential Infrastructure/Transient Issues:

  • CoverageWarning: No data was collected:
    This warning from coverage.py suggests that the coverage tool might not have been able to collect any data during the test run. This can sometimes happen due to test execution issues or problems with the coverage setup itself.

Fix: This is likely a transient issue or related to the other test failures. Once the primary test failures are resolved, this warning may disappear. If it persists, investigate the coverage configuration.

Summary:

The primary issues appear to be related to datetime handling, missing test assets (floorplan images), and errors in the template cloning logic, which may be cascading into other tests. Addressing these specific areas should help resolve the majority of the failures.

@openwisp-companion
Copy link
Copy Markdown

Dependency Installation Script Error

Hello @pandafy,
(Analysis for commit a70f49d)

The CI failed due to an error in the dependency installation script, specifically an "unexpected EOF while looking for matching "". This indicates a syntax error in the script itself, likely a missing quote character.

Fix:
Review the dependency installation commands in the CI script for any unclosed quotation marks. The specific line causing the issue appears to be related to the pip install -UI --force-reinstall --no-cache-dir --no-deps "openwisp-utils @ https://github.com/openwisp/openwisp-utils/archive/refs/heads/issues/633-ValidatedModelSerializer.tar.gz command, which might be missing a closing quote.

@pandafy pandafy force-pushed the issues/1224-separate-geo-logic-from-config branch from a70f49d to df49464 Compare March 25, 2026 10:09
@openwisp-companion
Copy link
Copy Markdown

Multiple Test Failures Detected

Hello @pandafy,
(Analysis for commit df49464)

There are multiple test failures in the CI logs.

1. Runtime Warnings (Naive Datetimes):
Several RuntimeWarning: DateTimeField ... received a naive datetime ... while time zone support is active. warnings are present. This indicates that naive datetime objects are being used where timezone-aware datetimes are expected.

  • Fix: Ensure all datetime objects used with DateTimeFields are timezone-aware. This typically involves using timezone.now() or ensuring datetime objects are properly localized.

2. Test Failures (Marked with 'E' or 'F'):
The logs indicate several test failures, though the specific error messages are truncated. The presence of 'E' and 'F' in the test output suggests errors and failures.

  • Fix:
  • Examine the full test output to identify the specific failing tests and their error messages.
  • Common causes include incorrect test logic, assertion errors, or issues with test data setup.
  • If the failures are related to the geo or connection apps (as suggested by the file paths in the code context), review the recent changes in those areas.

3. Floorplan Image Not Found Errors:
Multiple errors indicate that floorplan images are not found (floorplan image not found while showing floorplan: floorplans/...jpg).

  • Fix: Ensure that the floorplan image files referenced in the tests are correctly placed and accessible in the test environment. This might involve checking test data setup or file paths.

4. Slow Tests:
The logs mention Total slow tests detected: 497. While not a failure, a large number of slow tests can indicate performance issues or inefficient test setups.

  • Fix: Review the tests marked as slow to identify potential optimizations. This could involve refactoring tests, improving query efficiency, or reducing unnecessary operations.

5. Coverage Warning:
A CoverageWarning: No data was collected. indicates that code coverage was not properly measured.

  • Fix: Ensure that the coverage collection is correctly configured and running for all tests. This might involve checking the CI configuration or the runtests.py script.

To address these issues, please:

  1. Investigate the specific error messages for the 'E' and 'F' marked tests.
  2. Verify floorplan image paths and test data setup.
  3. Review datetime handling to ensure timezone awareness.
  4. Optimize slow tests if possible.
  5. Ensure coverage collection is working correctly.

@pandafy pandafy force-pushed the issues/1224-separate-geo-logic-from-config branch from df49464 to 9f4b5f3 Compare March 25, 2026 10:32
@coveralls
Copy link
Copy Markdown

coveralls commented Mar 25, 2026

Coverage Status

coverage: 98.588% (-0.07%) from 98.66% — issues/1224-separate-geo-logic-from-config into master

@pandafy pandafy marked this pull request as ready for review March 25, 2026 11:28
@openwisp-companion
Copy link
Copy Markdown

QA Failures in Multiple Jobs

Hello @pandafy,
(Analysis for commit 1d60aa4)

  • Code Style/QA: The CI failed due to incorrectly sorted or formatted imports in openwisp_controller/geo/api/serializers.py. Please run openwisp-qa-format to fix this.

@pandafy pandafy force-pushed the issues/1224-separate-geo-logic-from-config branch from 1d60aa4 to 8d38dee Compare March 25, 2026 14:09
@pandafy pandafy force-pushed the issues/1224-separate-geo-logic-from-config branch from 2de0490 to a558103 Compare April 6, 2026 17:30
@openwisp-companion
Copy link
Copy Markdown

CI Failures: Code Style and Imports

Hello @pandafy,
(Analysis for commit 2de0490)

There are import sorting and formatting issues detected by isort in the following file:

  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py: Imports are incorrectly sorted and/or formatted.

Fix:
Please run openwisp-qa-format to automatically fix these issues.

@openwisp-companion
Copy link
Copy Markdown

CI Failures: Code Style and Imports

Hello @pandafy,
(Analysis for commit a558103)

The CI failed due to an isort check failure in the following file:

  • openwisp_controller/geo/migrations/0005_organizationgeosettings.py: Imports are incorrectly sorted and/or formatted.

To fix this, please run the following command in your local environment:

openwisp-qa-format

This command will automatically format your code and sort the imports according to the project's standards.

@pandafy pandafy force-pushed the issues/1224-separate-geo-logic-from-config branch from a558103 to 7f5827a Compare April 6, 2026 18:00
Backward incompatible change within the master branch
(still backward compatible to previous stable releases)
The estimated location feature now has its own
MESSAGE_MAP and its own helper for sending notifications,
clearly separating geo.estimated_location from config.whois
while maintainig a very similar code structure.
Comment thread openwisp_controller/geo/base/models.py Outdated
)
}
)
return super().clean()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems unneeded, I am going to remove it.

Comment thread openwisp_controller/geo/base/models.py Outdated
return self.device.organization_id


class AbstractOrganizationGeoSettings(UUIDModel):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should do like we have done in other modules where we don't add a new primary key, relying on the organization one-to-one relation to be the primary key.

@openwisp-companion
Copy link
Copy Markdown

Test Failure in test_organization_geo_settings_retrieve

Hello @pandafy and @nemesifier,
(Analysis for commit fd4dc2d)

The test test_organization_geo_settings_retrieve in openwisp_controller/geo/tests/test_api.py failed with a KeyError: 'id'.

This indicates that the data dictionary returned by the API call does not contain the expected 'id' key. This is likely because the API response for organization geo settings is missing the 'id' field, or the test is asserting against an incorrect structure.

Fix:

  1. Inspect the API response: Add a print statement or use a debugger to inspect the data variable in the test_organization_geo_settings_retrieve test to understand its actual structure.
  2. Adjust the assertion: If the 'id' key is indeed missing or has a different name, update the assertion self.assertEqual(data["id"], str(org1_geo_settings.pk)) accordingly. If the 'id' field is expected but missing, investigate the OrganizationGeoSettingsSerializer and the view (OrganizationGeoSettingsView) to ensure the 'id' is correctly included in the serialized output.

@nemesifier nemesifier force-pushed the issues/1224-separate-geo-logic-from-config branch from 401d070 to 4e8bf0a Compare April 6, 2026 21:48
@openwisp openwisp deleted a comment from openwisp-companion bot Apr 6, 2026
Copy link
Copy Markdown
Member Author

@pandafy pandafy left a comment

Choose a reason for hiding this comment

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

Thank you for resolving the bugs in my implementation @nemesifier! It looks good to merge now.

@nemesifier nemesifier changed the title [chores:bug] Separated openwisp_controller.geo from openwisp_controller.config #1224 [chores:bug] Separated geo.estimated_location from config.whois #1224 Apr 7, 2026
@nemesifier nemesifier changed the title [chores:bug] Separated geo.estimated_location from config.whois #1224 [chores:fix] Separated geo.estimated_location from config.whois #1224 Apr 7, 2026
@nemesifier nemesifier merged commit b8c4d3c into master Apr 7, 2026
33 of 34 checks passed
@nemesifier nemesifier deleted the issues/1224-separate-geo-logic-from-config branch April 7, 2026 12:53
@github-project-automation github-project-automation bot moved this from In progress to Done in OpenWISP Contributor's Board Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

[chores:bug] Separate geo.estimated_location from config.whois

3 participants