Skip to content

[change] Added generic enforcement of deactivated devices #1338#1365

Open
pandafy wants to merge 15 commits into
masterfrom
issues/1338-generic-deactivated-device
Open

[change] Added generic enforcement of deactivated devices #1338#1365
pandafy wants to merge 15 commits into
masterfrom
issues/1338-generic-deactivated-device

Conversation

@pandafy
Copy link
Copy Markdown
Member

@pandafy pandafy commented May 18, 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 #1338

Description of Changes

Added generic enforcement of deactivated device

…#1325

This commit adds regression tests to ensure that the WHOIS handling
for deactivated devices works as expected.
…#1325

This commit adds regression tests to ensure that the WHOIS handling
for deactivated devices works as expected.
- add guard in WHOISService to skip processing for deactivated devices
- add guard in EstimatedLocationService to prevent task scheduling
- fix N+1 query by including _is_deactivated in clear_last_ip_command
  queryset
- add regression tests ensuring tasks are skipped when device is
  deactivated
- ensure deterministic tests by enabling whois on organization
  settings

Fixes #1325
@pandafy pandafy self-assigned this May 18, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR systematically enforces deactivation checks across OpenWISP controller to prevent operations on deactivated devices. It introduces an is_fully_deactivated() helper method and applies it consistently: bulk operations (template application, group management) skip deactivated devices silently, while explicit operations (API endpoints, command execution) reject them with clear errors. Guards are added to WHOIS processing, connection operations, background tasks, and estimated location handlers. Querysets are optimized to avoid N+1 queries when signal handlers call deactivation checks. Comprehensive test coverage validates both skip and reject behaviors across device management, connections, WHOIS, and subnet division workflows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • nemesifier
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description is minimal and vague ('Added generic enforcement of deactivated device'), lacking concrete details about what enforcement mechanisms were added and how. Expand the description to document the canonical method(s) added (e.g., is_deactivated(), is_fully_deactivated()), the enforcement approach adopted across modules, and key implementation decisions.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding generic enforcement of deactivated devices, and references the linked issue #1338.
Linked Issues check ✅ Passed The PR successfully addresses all three core outcomes from issue #1338: introduces is_deactivated() and is_fully_deactivated() methods for checking device operability; implements consistent rejection/skipping of deactivated devices across all major code paths (API endpoints, background tasks, configuration/template logic, workflows); uses clear error handling (RuntimeError, ValidationError, failed command status).
Out of Scope Changes check ✅ Passed All changes directly support deactivated device enforcement: new methods, early returns in operations, signal handlers, test cases, and query optimizations with _is_deactivated field all align with the PR's objectives.
Bug Fixes ✅ Passed PR is an enhancement (adding deactivated device enforcement), not a bug fix. The Bug Fixes custom check is not applicable to feature/enhancement PRs.

✏️ 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/1338-generic-deactivated-device

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: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_controller/config/base/device_group.py`:
- Line 113: The loop currently iterates over
device_group.device_set.exclude(_is_deactivated=True).iterator() which misses
other non-operable states; update the query on device_group.device_set to use
the project's canonical operability filter/manager (e.g., the operable()
queryset or the is_operable/is_active boolean field or provided manager method)
instead of only excluding _is_deactivated, so the iterator only yields truly
operable devices (replace the exclude(...) call with the canonical operability
filter and keep the iterator()).

In `@openwisp_controller/config/controller/views.py`:
- Around line 400-402: The guard currently checks device.is_deactivated() which
allows config-level deactivations to be bypassed; change the condition to call
the canonical strict helper device.is_fully_deactivated() instead so the
ControllerResponse("error: device deactivated", status=403) is returned for
fully-deactivated devices; locate this in the view where device =
self.model.objects.select_related("config").get(key=key) and replace the
is_deactivated() call with is_fully_deactivated() (no other behavior changes
needed).

In `@openwisp_controller/connection/base/models.py`:
- Around line 349-350: The hardcoded error message in the RuntimeError raised
after the self.device.is_fully_deactivated() check must be marked for
translation; import Django's i18n helper (e.g. from django.utils.translation
import gettext_lazy as _) in openwisp_controller/connection/base/models.py and
replace RuntimeError("Device is deactivated") with RuntimeError(_("Device is
deactivated")) so the failure_reason stored can be translated when surfaced.
- Around line 470-471: Replace the call to self.device.is_deactivated() with the
stricter self.device.is_fully_deactivated() to match other deactivation checks
(e.g., the ones around is_fully_deactivated() used elsewhere in this class), and
add a defensive guard to ensure self.device is not None before calling the
method so the validation block handles cases where device isn't set yet; update
the ValidationError to remain the same field ("device") and message if the
fully-deactivated check returns True.

In `@openwisp_controller/geo/estimated_location/handlers.py`:
- Around line 71-75: The condition in the handler uses the method object
device.is_deactivated instead of calling it, causing the if to always be truthy;
update the condition to invoke the method (device.is_deactivated()) so it
behaves like the other callsites, and keep the rest of the check
(EstimatedLocationService.check_estimated_location_enabled(device.organization_id))
unchanged.
🪄 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: 7694feac-56b3-419e-bd3e-36f00e30d24b

📥 Commits

Reviewing files that changed from the base of the PR and between c3ea70f and 635306e.

📒 Files selected for processing (12)
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/tasks.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). (12)
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | 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~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Analyze (javascript-typescript)
🧰 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/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/geo/estimated_location/handlers.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/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/geo/estimated_location/handlers.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/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/geo/estimated_location/handlers.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/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/geo/estimated_location/handlers.py
🧠 Learnings (4)
📚 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/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/geo/estimated_location/handlers.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/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/geo/estimated_location/handlers.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/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/geo/estimated_location/handlers.py
📚 Learning: 2026-03-27T20:50:26.240Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1315
File: openwisp_controller/geo/estimated_location/service.py:70-76
Timestamp: 2026-03-27T20:50:26.240Z
Learning: In openwisp-controller’s WHOIS and estimated-location services (openwisp_controller/config/whois/ and openwisp_controller/geo/estimated_location/), these components only process public IP addresses. When reviewing logs/error/debug messages in this area, treat logging the IP address as acceptable and do not flag it as a privacy/security concern—unless the logged value can originate from non-public/private IPs in that specific code path.

Applied to files:

  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/geo/estimated_location/handlers.py
🔇 Additional comments (10)
openwisp_controller/connection/tests/test_api.py (1)

290-290: LGTM!

openwisp_controller/config/base/device.py (1)

188-191: LGTM!

Also applies to: 307-307, 484-484

openwisp_controller/config/base/config.py (1)

999-1000: LGTM!

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

213-214: LGTM!

openwisp_controller/config/whois/tasks.py (1)

75-77: LGTM!

Also applies to: 120-121

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

124-128: LGTM!

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

46-46: LGTM!

openwisp_controller/connection/base/models.py (1)

144-145: LGTM!

Also applies to: 271-274, 384-386

openwisp_controller/connection/tasks.py (1)

49-51: LGTM!

openwisp_controller/connection/apps.py (1)

92-93: LGTM!

templates = Template.objects.filter(pk__in=template_ids)
old_templates = Template.objects.filter(pk__in=old_template_ids)
for device in device_group.device_set.iterator():
for device in device_group.device_set.exclude(_is_deactivated=True).iterator():
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 | ⚡ Quick win

Use the canonical operability criteria for device filtering.

At Line 113, filtering only on _is_deactivated=True can still include devices whose config is already non-operable (for example, deactivating/deactivated), so group template updates may run on devices that should be skipped.

Suggested change
-        for device in device_group.device_set.exclude(_is_deactivated=True).iterator():
+        for device in (
+            device_group.device_set.exclude(_is_deactivated=True)
+            .exclude(config__status__in=["deactivating", "deactivated"])
+            .iterator()
+        ):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/base/device_group.py` at line 113, The loop
currently iterates over
device_group.device_set.exclude(_is_deactivated=True).iterator() which misses
other non-operable states; update the query on device_group.device_set to use
the project's canonical operability filter/manager (e.g., the operable()
queryset or the is_operable/is_active boolean field or provided manager method)
instead of only excluding _is_deactivated, so the iterator only yields truly
operable devices (replace the exclude(...) call with the canonical operability
filter and keep the iterator()).

Comment on lines +400 to +402
device = self.model.objects.select_related("config").get(key=key)
if device.is_deactivated():
return ControllerResponse("error: device deactivated", status=403)
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 | ⚡ Quick win

Use the canonical strict deactivation helper here

At Line 401, this guard should call the shared strict helper (is_fully_deactivated()) rather than is_deactivated() to keep behavior aligned with the stack’s canonical enforcement contract and avoid allowing re-registration on config-level deactivation paths.

Suggested fix
-            if device.is_deactivated():
+            if device.is_fully_deactivated():
                 return ControllerResponse("error: device deactivated", status=403)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/controller/views.py` around lines 400 - 402, The
guard currently checks device.is_deactivated() which allows config-level
deactivations to be bypassed; change the condition to call the canonical strict
helper device.is_fully_deactivated() instead so the ControllerResponse("error:
device deactivated", status=403) is returned for fully-deactivated devices;
locate this in the view where device =
self.model.objects.select_related("config").get(key=key) and replace the
is_deactivated() call with is_fully_deactivated() (no other behavior changes
needed).

Comment on lines +349 to +350
if self.device.is_fully_deactivated():
raise RuntimeError("Device is deactivated")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the deactivation failure message translatable.

At Line 350, the new string is stored in failure_reason and can surface in notifications; mark it for i18n.

Proposed fix
-                raise RuntimeError("Device is deactivated")
+                raise RuntimeError(gettext("Device is deactivated."))

As per coding guidelines: "For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/connection/base/models.py` around lines 349 - 350, The
hardcoded error message in the RuntimeError raised after the
self.device.is_fully_deactivated() check must be marked for translation; import
Django's i18n helper (e.g. from django.utils.translation import gettext_lazy as
_) in openwisp_controller/connection/base/models.py and replace
RuntimeError("Device is deactivated") with RuntimeError(_("Device is
deactivated")) so the failure_reason stored can be translated when surfaced.

Comment thread openwisp_controller/connection/base/models.py
Comment thread openwisp_controller/geo/estimated_location/handlers.py
@openwisp-companion
Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

@pandafy pandafy force-pushed the issues/1338-generic-deactivated-device branch from 635306e to dccd050 Compare May 29, 2026 18:09
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

♻️ Duplicate comments (2)
openwisp_controller/connection/base/models.py (2)

360-364: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mark the deactivation error message as translatable.

The error message at line 364 will be stored in failure_reason (line 368) and surfaced to users in notifications. It must be translatable. Additionally, add a period for consistency with the error message at line 485.

🌐 Proposed fix
-            if self.device.is_fully_deactivated():
-                raise RuntimeError("Device is deactivated")
+            if self.device.is_fully_deactivated():
+                raise RuntimeError(gettext("Device is deactivated."))

As per coding guidelines: "For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/connection/base/models.py` around lines 360 - 364, The
RuntimeError message raised in the is_fully_deactivated check is not marked for
translation and lacks a trailing period; import Django's i18n (e.g. from
django.utils.translation import gettext_lazy as _) and replace the literal
string in the RuntimeError raised in the method containing
self.device.is_fully_deactivated() with a translatable string including a
period, e.g. RuntimeError(_("Device is deactivated.")), ensuring the unique
symbols self.device.is_fully_deactivated and the RuntimeError raise are updated
accordingly.

484-485: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use is_fully_deactivated() for consistency and add defensive check.

Line 484 uses is_deactivated(), while the same class uses is_fully_deactivated() at lines 363 and 398. For consistency with the operability contract elsewhere in this file, use the stricter is_fully_deactivated() check.

Additionally, while the device field is required (no null=True at line 416), adding a defensive device_id check prevents potential AttributeError if clean() is called before the device is fully set during form validation.

♻️ Proposed fix
 def clean(self):
-    if self.device.is_deactivated():
+    if self.device_id and self.device.is_fully_deactivated():
         raise ValidationError({"device": _("Device is deactivated.")})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/connection/base/models.py` around lines 484 - 485,
Replace the use of is_deactivated() with is_fully_deactivated() to match the
class's operability contract and add a defensive check for device_id before
calling any device methods inside the clean() method: if self.device_id is
falsy, skip the deactivation check; otherwise call
self.device.is_fully_deactivated() and raise ValidationError({"device":
_("Device is deactivated.")}) when true. Update the clause that currently
references is_deactivated() accordingly to use is_fully_deactivated() and guard
with a device_id existence check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_controller/geo/tests/utils.py`:
- Line 14: Replace the inefficient count-based existence check with .exists():
change the condition in the utility where
Organization.objects.filter(**options).count() is used to use
Organization.objects.filter(**options).exists() instead (i.e., update the line
starting with if not Organization.objects.filter(**options).count(): to if not
Organization.objects.filter(**options).exists():), keeping the same surrounding
logic and using the same variables (Organization, options).

In `@openwisp_controller/subnet_division/rule_types/device.py`:
- Around line 57-60: The filter call using the lookup device___is_deactivated is
correct but non-obvious; update the iterator/filter in the code that builds the
queryset (the call with
.filter(device__organization_id=rule_obj.organization_id,
device___is_deactivated=False).iterator()) to add a concise inline comment
explaining the triple underscore: the pattern is device__{related_field_name}
where the related field itself is named _is_deactivated, so the extra underscore
is part of the field name; place the comment immediately next to
device___is_deactivated to prevent future regressions.

---

Duplicate comments:
In `@openwisp_controller/connection/base/models.py`:
- Around line 360-364: The RuntimeError message raised in the
is_fully_deactivated check is not marked for translation and lacks a trailing
period; import Django's i18n (e.g. from django.utils.translation import
gettext_lazy as _) and replace the literal string in the RuntimeError raised in
the method containing self.device.is_fully_deactivated() with a translatable
string including a period, e.g. RuntimeError(_("Device is deactivated.")),
ensuring the unique symbols self.device.is_fully_deactivated and the
RuntimeError raise are updated accordingly.
- Around line 484-485: Replace the use of is_deactivated() with
is_fully_deactivated() to match the class's operability contract and add a
defensive check for device_id before calling any device methods inside the
clean() method: if self.device_id is falsy, skip the deactivation check;
otherwise call self.device.is_fully_deactivated() and raise
ValidationError({"device": _("Device is deactivated.")}) when true. Update the
clause that currently references is_deactivated() accordingly to use
is_fully_deactivated() and guard with a device_id existence check.
🪄 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: 4794e788-09aa-4fad-bdce-7f5720a509f0

📥 Commits

Reviewing files that changed from the base of the PR and between 635306e and dccd050.

📒 Files selected for processing (18)
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/subnet_division/rule_types/device.py
  • openwisp_controller/subnet_division/tests/test_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). (11)
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.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~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.13 | 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/base/device_group.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/subnet_division/rule_types/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/base/models.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/base/device_group.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/subnet_division/rule_types/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/base/models.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/base/device_group.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/subnet_division/rule_types/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/base/models.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/base/device_group.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/subnet_division/rule_types/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/base/models.py
🧠 Learnings (4)
📚 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/base/device_group.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/subnet_division/rule_types/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/base/models.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/base/device_group.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/subnet_division/rule_types/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/base/models.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/base/device_group.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/apps.py
  • openwisp_controller/subnet_division/rule_types/device.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/base/models.py
📚 Learning: 2026-03-27T20:50:26.240Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1315
File: openwisp_controller/geo/estimated_location/service.py:70-76
Timestamp: 2026-03-27T20:50:26.240Z
Learning: In openwisp-controller’s WHOIS and estimated-location services (openwisp_controller/config/whois/ and openwisp_controller/geo/estimated_location/), these components only process public IP addresses. When reviewing logs/error/debug messages in this area, treat logging the IP address as acceptable and do not flag it as a privacy/security concern—unless the logged value can originate from non-public/private IPs in that specific code path.

Applied to files:

  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/whois/tasks.py
🔇 Additional comments (19)
openwisp_controller/config/base/device_group.py (1)

113-113: Use the canonical operability criteria for device filtering.

The current filter only excludes devices where _is_deactivated=True, but does not account for devices whose config status is deactivating or deactivated. This means group template updates may still attempt to process devices that are not fully operable.

Based on the past review comment, consider excluding devices with non-operable config states as well.

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

213-214: LGTM!

openwisp_controller/connection/tasks.py (1)

49-51: LGTM!

Also applies to: 82-89

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

47-50: LGTM!

openwisp_controller/connection/tests/test_tasks.py (1)

166-183: LGTM!

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

124-128: LGTM!

openwisp_controller/config/tests/test_device.py (1)

515-531: LGTM!

openwisp_controller/connection/tests/test_models.py (1)

352-360: LGTM!

Also applies to: 1176-1196

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

46-49: LGTM!

Also applies to: 71-77

openwisp_controller/config/whois/tasks.py (1)

75-77: LGTM!

Also applies to: 120-126

openwisp_controller/config/controller/views.py (1)

124-126: LGTM!

Also applies to: 137-139, 402-404

openwisp_controller/subnet_division/tests/test_models.py (1)

818-844: LGTM!

openwisp_controller/connection/apps.py (1)

92-93: LGTM!

openwisp_controller/subnet_division/rule_types/device.py (1)

36-42: LGTM!

openwisp_controller/config/base/config.py (1)

485-490: LGTM!

Also applies to: 1004-1005

openwisp_controller/config/base/device.py (1)

188-191: LGTM!

Also applies to: 307-308, 484-485, 504-508

openwisp_controller/connection/tests/test_api.py (1)

290-290: LGTM!

openwisp_controller/connection/base/models.py (2)

144-147: LGTM!

Also applies to: 186-188, 276-280


387-388: LGTM!

Also applies to: 398-399

options.update(kwargs)
options.setdefault("slug", slugify(options["name"]))
if not Organization.objects.filter(**kwargs).count():
if not Organization.objects.filter(**options).count():
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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Prefer .exists() over .count() for boolean checks.

Line 14 uses .count() to check if an organization exists. The .exists() method is more efficient as it stops at the first match rather than counting all matching rows.

⚡ Suggested refactor
-        if not Organization.objects.filter(**options).count():
+        if not Organization.objects.filter(**options).exists():
📝 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
if not Organization.objects.filter(**options).count():
if not Organization.objects.filter(**options).exists():
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/geo/tests/utils.py` at line 14, Replace the inefficient
count-based existence check with .exists(): change the condition in the utility
where Organization.objects.filter(**options).count() is used to use
Organization.objects.filter(**options).exists() instead (i.e., update the line
starting with if not Organization.objects.filter(**options).count(): to if not
Organization.objects.filter(**options).exists():), keeping the same surrounding
logic and using the same variables (Organization, options).

Comment on lines +57 to +60
.filter(
device__organization_id=rule_obj.organization_id,
device___is_deactivated=False,
).iterator()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the triple-underscore ORM lookup at Line 59.

device___is_deactivated is valid but non-obvious. Add a short inline note that the third underscore comes from the _is_deactivated field name to prevent accidental regressions.

Suggested patch
             .filter(
                 device__organization_id=rule_obj.organization_id,
+                # Triple underscore is intentional:
+                # `device__` relation lookup + `_is_deactivated` field name.
                 device___is_deactivated=False,
             ).iterator()
As per coding guidelines: "Cryptic or non-obvious code ... must include a concise comment explaining why it is needed and why the complexity is acceptable".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/subnet_division/rule_types/device.py` around lines 57 -
60, The filter call using the lookup device___is_deactivated is correct but
non-obvious; update the iterator/filter in the code that builds the queryset
(the call with .filter(device__organization_id=rule_obj.organization_id,
device___is_deactivated=False).iterator()) to add a concise inline comment
explaining the triple underscore: the pattern is device__{related_field_name}
where the related field itself is named _is_deactivated, so the extra underscore
is part of the field name; place the comment immediately next to
device___is_deactivated to prevent future regressions.

@pandafy pandafy force-pushed the issues/1338-generic-deactivated-device branch from dccd050 to 82b9640 Compare June 1, 2026 16:09
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

♻️ Duplicate comments (2)
openwisp_controller/config/controller/views.py (1)

409-411: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the canonical strict deactivation helper.

The guard checks device.is_deactivated() which only verifies device-level deactivation. For explicit operations like re-registration, use device.is_fully_deactivated() to ensure both device and config are checked, maintaining consistency with the PR's canonical enforcement contract.

🔒 Proposed fix
             device = self.model.objects.select_related("config").get(key=key)
-            if device.is_deactivated():
+            if device.is_fully_deactivated():
                 return ControllerResponse("error: device deactivated", status=403)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/controller/views.py` around lines 409 - 411,
Replace the soft deactivation check device.is_deactivated() with the stricter
helper device.is_fully_deactivated() in the view that fetches a device via
self.model.objects.select_related("config").get(key=key) so that both device-
and config-level deactivation are enforced; update the conditional and the
returned ControllerResponse to remain the same but gated by
device.is_fully_deactivated() to follow the canonical enforcement contract.
openwisp_controller/connection/base/models.py (1)

364-364: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the deactivation failure message translatable.

The string "Device is deactivated" is stored in failure_reason and can surface in notifications. Mark it for i18n using gettext().

Proposed fix
-                raise RuntimeError("Device is deactivated")
+                raise RuntimeError(gettext("Device is deactivated."))

As per coding guidelines: "For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/connection/base/models.py` at line 364, Wrap the
user-facing literal "Device is deactivated" in Django's i18n call and ensure the
translator import is present: add "from django.utils.translation import gettext
as _" (if not already imported) and change the exception to raise
RuntimeError(_("Device is deactivated")) so the value stored in failure_reason
is translatable; update the raise site in the model where failure_reason is set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openwisp_controller/config/base/device_group.py`:
- Around line 113-116: Remove the duplicate loop and iterate devices only once;
replace the erroneous second "for device in device_group.device_set.iterator()"
with a single iteration that first calls device.create_default_config() for
devices excluded by
device_group.device_set.iterator().exclude(_is_deactivated=True) and then only
calls device.config.manage_group_templates(templates, old_templates) for devices
that are not fully deactivated (use the same logic as is_fully_deactivated() or
call that helper if available). Ensure you reference the methods
device.create_default_config, device.config.manage_group_templates, and the
is_fully_deactivated logic when applying the filtering so
deactivating/deactivated devices are skipped appropriately.

In `@openwisp_controller/config/whois/service.py`:
- Line 217: The WHOIS code currently bails out on self.device.is_deactivated()
which triggers as soon as Device.deactivate() begins, causing a gap while the
associated Config is still in deactivating; change the guards in
openwisp_controller/config/whois/service.py (the self.device.is_deactivated()
check) and in openwisp_controller/config/whois/tasks.py to use
self.device.is_fully_deactivated() so WHOIS only skips once the device and its
config are fully deactivated, and add a test that creates a Device whose Config
is in deactivating state (config.deactivate() started but not finished) to
assert WHOIS continues to run until is_fully_deactivated() becomes true.

---

Duplicate comments:
In `@openwisp_controller/config/controller/views.py`:
- Around line 409-411: Replace the soft deactivation check
device.is_deactivated() with the stricter helper device.is_fully_deactivated()
in the view that fetches a device via
self.model.objects.select_related("config").get(key=key) so that both device-
and config-level deactivation are enforced; update the conditional and the
returned ControllerResponse to remain the same but gated by
device.is_fully_deactivated() to follow the canonical enforcement contract.

In `@openwisp_controller/connection/base/models.py`:
- Line 364: Wrap the user-facing literal "Device is deactivated" in Django's
i18n call and ensure the translator import is present: add "from
django.utils.translation import gettext as _" (if not already imported) and
change the exception to raise RuntimeError(_("Device is deactivated")) so the
value stored in failure_reason is translatable; update the raise site in the
model where failure_reason is set.
🪄 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: c8dbda19-e997-4a38-b398-03c0209bdd8b

📥 Commits

Reviewing files that changed from the base of the PR and between dccd050 and 82b9640.

📒 Files selected for processing (20)
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/config/whois/commands.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/config/whois/tests/tests.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/geo/estimated_location/handlers.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/tests/utils.py
  • openwisp_controller/subnet_division/tests/test_models.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/config/whois/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/whois/commands.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/whois/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/config/whois/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/whois/commands.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/whois/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/config/whois/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/whois/commands.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/whois/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/config/whois/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/whois/commands.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/whois/tests/tests.py
🧠 Learnings (4)
📚 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/whois/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/whois/commands.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/whois/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/config/whois/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/whois/commands.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/whois/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/config/whois/tasks.py
  • openwisp_controller/geo/tests/utils.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/config/tests/test_device.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/connection/tasks.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/config/base/device.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/whois/commands.py
  • openwisp_controller/config/controller/views.py
  • openwisp_controller/config/base/device_group.py
  • openwisp_controller/connection/tests/test_models.py
  • openwisp_controller/subnet_division/tests/test_models.py
  • openwisp_controller/connection/tests/test_tasks.py
  • openwisp_controller/config/base/config.py
  • openwisp_controller/connection/base/models.py
  • openwisp_controller/connection/tests/test_api.py
  • openwisp_controller/config/whois/tests/tests.py
📚 Learning: 2026-03-27T20:50:26.240Z
Learnt from: nemesifier
Repo: openwisp/openwisp-controller PR: 1315
File: openwisp_controller/geo/estimated_location/service.py:70-76
Timestamp: 2026-03-27T20:50:26.240Z
Learning: In openwisp-controller’s WHOIS and estimated-location services (openwisp_controller/config/whois/ and openwisp_controller/geo/estimated_location/), these components only process public IP addresses. When reviewing logs/error/debug messages in this area, treat logging the IP address as acceptable and do not flag it as a privacy/security concern—unless the logged value can originate from non-public/private IPs in that specific code path.

Applied to files:

  • openwisp_controller/config/whois/tasks.py
  • openwisp_controller/geo/estimated_location/tests/tests.py
  • openwisp_controller/geo/estimated_location/service.py
  • openwisp_controller/config/whois/service.py
  • openwisp_controller/geo/estimated_location/handlers.py
  • openwisp_controller/geo/estimated_location/tasks.py
  • openwisp_controller/config/whois/commands.py
  • openwisp_controller/config/whois/tests/tests.py
🔇 Additional comments (30)
openwisp_controller/config/base/device.py (4)

188-191: LGTM!


307-307: LGTM!


484-487: LGTM!


508-512: LGTM!

openwisp_controller/config/base/config.py (2)

558-562: LGTM!


860-868: LGTM!

openwisp_controller/config/controller/views.py (2)

96-100: LGTM!


115-118: LGTM!

openwisp_controller/config/whois/tasks.py (1)

122-129: LGTM!

openwisp_controller/config/whois/commands.py (1)

40-44: LGTM!

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

144-147: LGTM!


186-188: LGTM!


276-280: LGTM!


398-399: LGTM!


557-564: LGTM!

openwisp_controller/connection/tasks.py (1)

49-51: LGTM!

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

66-68: LGTM!

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

46-46: LGTM!


71-78: LGTM!

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

124-130: LGTM!

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

14-14: Prefer .exists() for existence checks.

This was already flagged earlier and still applies at this line (.count() is doing more work than needed).


47-50: LGTM!

openwisp_controller/config/tests/test_device.py (1)

515-535: LGTM!

openwisp_controller/connection/tests/test_models.py (1)

352-360: LGTM!

Also applies to: 1176-1198

openwisp_controller/connection/tests/test_tasks.py (1)

166-185: LGTM!

openwisp_controller/config/whois/tests/tests.py (1)

180-184: LGTM!

Also applies to: 487-492, 506-509, 831-837, 881-886, 960-963, 979-984, 1012-1025, 1108-1167

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

197-212: LGTM!

Also applies to: 816-828

openwisp_controller/subnet_division/tests/test_models.py (1)

818-834: LGTM!

Also applies to: 835-850

openwisp_controller/connection/tests/test_api.py (2)

289-317: LGTM!


609-609: LGTM!

Comment thread openwisp_controller/config/base/device_group.py Outdated
# Do not trigger WHOIS fetch for deactivated devices.
# Returning here also suppresses the whois_lookup_skipped signal emitted
# below, so estimated location is not triggered for a deactivated device.
if self.device.is_deactivated():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare is_deactivated() vs is_fully_deactivated() usage in this PR
rg -n "is_(fully_)?deactivated\(\)" openwisp_controller/config/whois/ openwisp_controller/connection/

Repository: openwisp/openwisp-controller

Length of output: 911


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant surrounding code for each call site
targets=(
  "openwisp_controller/config/whois/service.py:217"
  "openwisp_controller/config/whois/service.py:240"
  "openwisp_controller/config/whois/tasks.py:77"
  "openwisp_controller/connection/tasks.py:49"
  "openwisp_controller/connection/base/models.py:363"
  "openwisp_controller/connection/base/models.py:398"
  "openwisp_controller/connection/base/models.py:484"
  "openwisp_controller/connection/base/models.py:560"
)

for t in "${targets[@]}"; do
  file="${t%%:*}"
  line="${t##*:}"
  echo "===== $file:$line ====="
  # print ~25 lines around target (compact)
  python3 - <<PY
import pathlib
p = pathlib.Path("${file}")
line = ${line}
start = max(1, line-20)
end = line+20
with p.open('r', encoding='utf-8') as f:
    for i, l in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:6d}: {l.rstrip()}")
        elif i > end:
            break
PY
  echo
done

Repository: openwisp/openwisp-controller

Length of output: 15931


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where the helpers are defined and any docs/comments describing them.
rg -n "def is_(fully_)?deactivated\\(" -S openwisp_controller
rg -n "is_(fully_)?deactivated\\(" -S openwisp_controller/config openwisp_controller/connection | head -n 200
rg -n "deactivat" -S openwisp_controller/config openwisp_controller/connection | head -n 200

Repository: openwisp/openwisp-controller

Length of output: 27644


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  "openwisp_controller/config/base/device.py"
  "openwisp_controller/config/base/config.py"
)

for f in "${files[@]}"; do
  echo "===== $f ====="
  python3 - <<PY
import pathlib, itertools
p=pathlib.Path("${f}")
# print around the deactivation helper methods
text=p.read_text(encoding='utf-8').splitlines()
for i,line in enumerate(text, start=1):
    if "def is_deactivated" in line or "def is_fully_deactivated" in line:
        start=max(1,i-15); end=min(len(text), i+40)
        for j in range(start,end+1):
            print(f"{j:6d}: {text[j-1].rstrip()}")
        print()
PY
done

Repository: openwisp/openwisp-controller

Length of output: 7469


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect WHOIS tests relevant to "skips when deactivated"
rg -n "test_.*(deactivated|deactivating|skips).*whois" openwisp_controller/config/whois/tests -S

# Show the key test bodies around the process/update skip assertions
python3 - <<'PY'
import pathlib, re

path = pathlib.Path("openwisp_controller/config/whois/tests/tests.py")
txt = path.read_text(encoding="utf-8").splitlines()
targets = []
for i,line in enumerate(txt, start=1):
    if "test_fetch_whois_details_skips_when_deactivated" in line or \
       "test_process_ip_skips_when_deactivated" in line or \
       "test_update_whois_skips_when_deactivated" in line:
        targets.append(i)
print("Targets:", targets)
for t in targets:
    start=max(1,t-25); end=min(len(txt), t+60)
    print("\n===== Context around line", t, "=====")
    for j in range(start,end+1):
        print(f"{j:6d}: {txt[j-1].rstrip()}")
PY

Repository: openwisp/openwisp-controller

Length of output: 54


🏁 Script executed:

#!/bin/bash
set +euo pipefail

ls -R openwisp_controller/config/whois/tests | head -n 200

rg -n "test_.*(deactivated|deactivating|skips).*whois" openwisp_controller/config/whois/tests -S || true

python3 - <<'PY'
import pathlib
p = pathlib.Path("openwisp_controller/config/whois/tests/tests.py")
print("File exists:", p.exists(), p)
txt = p.read_text(encoding="utf-8").splitlines()
need = [
    "test_fetch_whois_details_skips_when_deactivated",
    "test_process_ip_skips_when_deactivated",
    "test_update_whois_skips_when_deactivated",
]
for name in need:
    for i,line in enumerate(txt, start=1):
        if name in line:
            print("\n===== Context for", name, "at", i, "=====")
            for j in range(max(1,i-25), min(len(txt), i+70)+1):
                print(f"{j:6d}: {txt[j-1].rstrip()}")
            break
PY

Repository: openwisp/openwisp-controller

Length of output: 15960


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib
p=pathlib.Path("openwisp_controller/config/base/config.py")
txt=p.read_text(encoding="utf-8").splitlines()
for i,line in enumerate(txt, start=1):
    if "def deactivate" in line:
        start=max(1,i-10); end=min(len(txt), i+80)
        print("===== context", p, "around line", i, "=====")
        for j in range(start,end+1):
            print(f"{j:6d}: {txt[j-1].rstrip()}")
        break
PY

Repository: openwisp/openwisp-controller

Length of output: 4878


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect WHOIS test helpers to see what kind of device/config they create
rg -n "_create_device\\(" openwisp_controller/config/whois/tests -S
rg -n "class .*WHOIS" openwisp_controller/config/whois/tests/tests.py -S
sed -n '1,220p' openwisp_controller/config/whois/tests/utils.py

# Inspect WHOIS service signals/receivers usage (where whois_lookup_skipped is handled)
rg -n "whois_lookup_skipped" openwisp_controller/config/whois -S

Repository: openwisp/openwisp-controller

Length of output: 12018


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' openwisp_controller/config/whois/commands.py

Repository: openwisp/openwisp-controller

Length of output: 2764


🏁 Script executed:

rg -n "class CreateConfigMixin" -S openwisp_controller/config/tests openwisp_controller/config/whois/tests

Repository: openwisp/openwisp-controller

Length of output: 161


🏁 Script executed:

sed -n '1,140p' openwisp_controller/config/tests/utils.py

Repository: openwisp/openwisp-controller

Length of output: 4567


Clarify deactivation guard for WHOIS: is_deactivated() vs is_fully_deactivated()

openwisp_controller/config/whois/service.py returns early on self.device.is_deactivated() (e.g., lines ~217 and ~240). In openwisp_controller/config/base/device.py, is_deactivated() is just _is_deactivated and becomes True at the start of Device.deactivate(), while is_fully_deactivated() additionally requires config to be missing or config.is_deactivated() (config is set to deactivating during Config.deactivate()).

This means WHOIS currently skips during the “device deactivated but config still deactivating” window. openwisp_controller/config/whois/tasks.py uses the same is_deactivated() guard, and WHOIS tests cover the “no config” case (so they don’t exercise the deactivating-config window).

Confirm whether WHOIS should skip on any device deactivation, or only once the device is fully deactivated; if the latter, switch these guards to is_fully_deactivated() and add a test where the device has a config in deactivating status.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/whois/service.py` at line 217, The WHOIS code
currently bails out on self.device.is_deactivated() which triggers as soon as
Device.deactivate() begins, causing a gap while the associated Config is still
in deactivating; change the guards in
openwisp_controller/config/whois/service.py (the self.device.is_deactivated()
check) and in openwisp_controller/config/whois/tasks.py to use
self.device.is_fully_deactivated() so WHOIS only skips once the device and its
config are fully deactivated, and add a test that creates a Device whose Config
is in deactivating state (config.deactivate() started but not finished) to
assert WHOIS continues to run until is_fully_deactivated() becomes true.

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.

♻️ Duplicate comments (1)
openwisp_controller/config/base/device_group.py (1)

113-116: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use canonical full-deactivation checks when iterating group devices.

Filtering only by _is_deactivated=True can still process devices that are already non-operable due to config state (deactivating/deactivated). That breaks the “skip deactivated devices” contract for bulk template operations.

Suggested fix
-        for device in device_group.device_set.exclude(_is_deactivated=True).iterator():
+        for device in (
+            device_group.device_set.exclude(_is_deactivated=True)
+            .exclude(config__status__in=["deactivating", "deactivated"])
+            .iterator()
+        ):
             if not hasattr(device, "config"):
                 device.create_default_config()
             device.config.manage_group_templates(templates, old_templates)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openwisp_controller/config/base/device_group.py` around lines 113 - 116, When
iterating device_group.device_set to apply templates, exclude devices that are
fully non‑operational by filtering out both the internal flag and config
deactivation states; update the loop that uses
device_group.device_set.exclude(_is_deactivated=True).iterator() to also exclude
devices whose config state is 'deactivating' or 'deactivated' (e.g.,
.exclude(_is_deactivated=True).exclude(config__state__in=['deactivating','deactivated'])
or, if you prefer runtime checks, skip when device.config.state in
('deactivating','deactivated')) so device.create_default_config() and
device.config.manage_group_templates(…) are never called for fully deactivated
devices.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@openwisp_controller/config/base/device_group.py`:
- Around line 113-116: When iterating device_group.device_set to apply
templates, exclude devices that are fully non‑operational by filtering out both
the internal flag and config deactivation states; update the loop that uses
device_group.device_set.exclude(_is_deactivated=True).iterator() to also exclude
devices whose config state is 'deactivating' or 'deactivated' (e.g.,
.exclude(_is_deactivated=True).exclude(config__state__in=['deactivating','deactivated'])
or, if you prefer runtime checks, skip when device.config.state in
('deactivating','deactivated')) so device.create_default_config() and
device.config.manage_group_templates(…) are never called for fully deactivated
devices.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ee455462-e794-49b1-b2e2-ad1592627c1b

📥 Commits

Reviewing files that changed from the base of the PR and between 82b9640 and 1ae9781.

📒 Files selected for processing (2)
  • openwisp_controller/config/base/config.py
  • openwisp_controller/config/base/device_group.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/config/base/device_group.py
  • openwisp_controller/config/base/config.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/base/device_group.py
  • openwisp_controller/config/base/config.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/base/device_group.py
  • openwisp_controller/config/base/config.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/base/device_group.py
  • openwisp_controller/config/base/config.py
🧠 Learnings (3)
📚 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/base/device_group.py
  • openwisp_controller/config/base/config.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/base/device_group.py
  • openwisp_controller/config/base/config.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/base/device_group.py
  • openwisp_controller/config/base/config.py

@pandafy pandafy marked this pull request as ready for review June 1, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[change] Generic enforcement of deactivated devices

2 participants