[change] Added generic enforcement of deactivated devices #1338#1365
[change] Added generic enforcement of deactivated devices #1338#1365pandafy wants to merge 15 commits into
Conversation
…#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.
…an10/openwisp-controller into fix-whois-deactivated
- 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
📝 WalkthroughWalkthroughThis PR systematically enforces deactivation checks across OpenWISP controller to prevent operations on deactivated devices. It introduces an Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (12)
openwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/connection/apps.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/connection/apps.pyopenwisp_controller/connection/base/models.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/connection/apps.pyopenwisp_controller/connection/base/models.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/connection/apps.pyopenwisp_controller/connection/base/models.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/connection/apps.pyopenwisp_controller/connection/base/models.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/connection/apps.pyopenwisp_controller/connection/base/models.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/connection/apps.pyopenwisp_controller/connection/base/models.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/connection/apps.pyopenwisp_controller/connection/base/models.pyopenwisp_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.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_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(): |
There was a problem hiding this comment.
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()).
| device = self.model.objects.select_related("config").get(key=key) | ||
| if device.is_deactivated(): | ||
| return ControllerResponse("error: device deactivated", status=403) |
There was a problem hiding this comment.
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).
| if self.device.is_fully_deactivated(): | ||
| raise RuntimeError("Device is deactivated") |
There was a problem hiding this comment.
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.
|
The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3). |
635306e to
dccd050
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
openwisp_controller/connection/base/models.py (2)
360-364:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMark 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 winUse
is_fully_deactivated()for consistency and add defensive check.Line 484 uses
is_deactivated(), while the same class usesis_fully_deactivated()at lines 363 and 398. For consistency with the operability contract elsewhere in this file, use the stricteris_fully_deactivated()check.Additionally, while the
devicefield is required (nonull=Trueat line 416), adding a defensivedevice_idcheck prevents potentialAttributeErrorifclean()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
📒 Files selected for processing (18)
openwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/connection/apps.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/subnet_division/rule_types/device.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/apps.pyopenwisp_controller/subnet_division/rule_types/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/apps.pyopenwisp_controller/subnet_division/rule_types/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/apps.pyopenwisp_controller/subnet_division/rule_types/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/apps.pyopenwisp_controller/subnet_division/rule_types/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/apps.pyopenwisp_controller/subnet_division/rule_types/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/apps.pyopenwisp_controller/subnet_division/rule_types/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/apps.pyopenwisp_controller/subnet_division/rule_types/device.pyopenwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_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 isdeactivatingordeactivated. 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(): |
There was a problem hiding this comment.
🧹 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.
| 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).
| .filter( | ||
| device__organization_id=rule_obj.organization_id, | ||
| device___is_deactivated=False, | ||
| ).iterator() |
There was a problem hiding this comment.
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()🤖 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.
dccd050 to
82b9640
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
openwisp_controller/config/controller/views.py (1)
409-411:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the canonical strict deactivation helper.
The guard checks
device.is_deactivated()which only verifies device-level deactivation. For explicit operations like re-registration, usedevice.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 winMake the deactivation failure message translatable.
The string "Device is deactivated" is stored in
failure_reasonand can surface in notifications. Mark it for i18n usinggettext().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
📒 Files selected for processing (20)
openwisp_controller/config/base/config.pyopenwisp_controller/config/base/device.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/config/whois/commands.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/config/whois/tasks.pyopenwisp_controller/config/whois/tests/tests.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/geo/estimated_location/service.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/geo/estimated_location/tests/tests.pyopenwisp_controller/geo/tests/utils.pyopenwisp_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.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/geo/estimated_location/tests/tests.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/geo/estimated_location/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/base/device.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/whois/commands.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/geo/estimated_location/tests/tests.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/geo/estimated_location/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/base/device.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/whois/commands.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/geo/estimated_location/tests/tests.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/geo/estimated_location/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/base/device.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/whois/commands.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/geo/estimated_location/tests/tests.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/geo/estimated_location/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/base/device.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/whois/commands.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/geo/estimated_location/tests/tests.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/geo/estimated_location/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/base/device.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/whois/commands.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/geo/estimated_location/tests/tests.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/geo/estimated_location/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/base/device.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/whois/commands.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/geo/tests/utils.pyopenwisp_controller/geo/estimated_location/tests/tests.pyopenwisp_controller/config/tests/test_device.pyopenwisp_controller/geo/estimated_location/service.pyopenwisp_controller/connection/tasks.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/config/base/device.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/whois/commands.pyopenwisp_controller/config/controller/views.pyopenwisp_controller/config/base/device_group.pyopenwisp_controller/connection/tests/test_models.pyopenwisp_controller/subnet_division/tests/test_models.pyopenwisp_controller/connection/tests/test_tasks.pyopenwisp_controller/config/base/config.pyopenwisp_controller/connection/base/models.pyopenwisp_controller/connection/tests/test_api.pyopenwisp_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.pyopenwisp_controller/geo/estimated_location/tests/tests.pyopenwisp_controller/geo/estimated_location/service.pyopenwisp_controller/config/whois/service.pyopenwisp_controller/geo/estimated_location/handlers.pyopenwisp_controller/geo/estimated_location/tasks.pyopenwisp_controller/config/whois/commands.pyopenwisp_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!
| # 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(): |
There was a problem hiding this comment.
🧩 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
doneRepository: 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 200Repository: 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
doneRepository: 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()}")
PYRepository: 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
PYRepository: 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
PYRepository: 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 -SRepository: openwisp/openwisp-controller
Length of output: 12018
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,140p' openwisp_controller/config/whois/commands.pyRepository: openwisp/openwisp-controller
Length of output: 2764
🏁 Script executed:
rg -n "class CreateConfigMixin" -S openwisp_controller/config/tests openwisp_controller/config/whois/testsRepository: openwisp/openwisp-controller
Length of output: 161
🏁 Script executed:
sed -n '1,140p' openwisp_controller/config/tests/utils.pyRepository: 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.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
openwisp_controller/config/base/device_group.py (1)
113-116:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse canonical full-deactivation checks when iterating group devices.
Filtering only by
_is_deactivated=Truecan 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
📒 Files selected for processing (2)
openwisp_controller/config/base/config.pyopenwisp_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.pyopenwisp_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.pyopenwisp_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.pyopenwisp_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.pyopenwisp_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.pyopenwisp_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.pyopenwisp_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.pyopenwisp_controller/config/base/config.py
Checklist
Reference to Existing Issue
Closes #1338
Description of Changes
Added generic enforcement of deactivated device