Skip to content

[fix] Prevent 500 and warn about credentialless devices on mass upgrade #302#401

Open
Adityashandilya555 wants to merge 2 commits intoopenwisp:masterfrom
Adityashandilya555:fix/302-mass-upgrade-credentialless-devices
Open

[fix] Prevent 500 and warn about credentialless devices on mass upgrade #302#401
Adityashandilya555 wants to merge 2 commits intoopenwisp:masterfrom
Adityashandilya555:fix/302-mass-upgrade-credentialless-devices

Conversation

@Adityashandilya555
Copy link
Copy Markdown

Problem

When triggering a mass firmware upgrade, if any device (or all devices) lacked a DeviceConnection with SSH credentials, _get_upgrader_class() would call get_upgrader_class_for_device() on an uncredentialed device, raising an unhandled DoesNotExist and returning a 500 error.

Additionally, there was no UI feedback to inform administrators which devices would be skipped due to missing credentials.

Changes

base/models.py_get_upgrader_class()

  • Wraps the upgrade_operations path in try/except ObjectDoesNotExist
  • Uses an ORM subquery to find the first device with a valid SSH credential from both related_device_fw and firmwareless_devices, instead of blindly using the first device regardless of credentials

admin.pyupgrade_selected()

  • After dry_run(), identifies devices without valid SSH credentials from both querysets using ORM exclusion
  • Builds a list of up to MAX_CREDENTIALLESS_DISPLAY (5) devices with their admin change URLs
  • Calculates overflow count (credentialless_more) for the "and N more" message
  • Passes credentialless_devices, credentialless_count, and credentialless_more to the template context

templates/admin/upgrade_selected_confirmation.html

  • New errornote warning block shown when credentialless_count > 0
  • Lists each credentialless device as a clickable admin link
  • Shows "... and N more device(s) without credentials (not shown)." when the list is truncated

tests/test_admin.py

  • test_upgrade_intermediate_page_credentialless_related — credentialless device in related DeviceFirmware
  • test_upgrade_intermediate_page_credentialless_firmwareless — credentialless firmwareless device
  • test_upgrade_intermediate_page_all_credentialless — all devices lack credentials, no 500
  • test_upgrade_intermediate_page_credentialless_more_than_max — overflow beyond display limit triggers "and N more"

Fixes

Fixes #302

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The mass-upgrade confirmation flow now detects devices without SSH credentials and reports them to the confirmation template. The admin action collects valid device IDs from enabled DeviceConnection rows whose update_strategy contains "ssh", excludes those from dry-run results to derive credentialless device lists, caps the displayed list at MAX_CREDENTIALLESS_DISPLAY = 5, and exposes credentialless_devices, credentialless_count, and credentialless_more in the template context. BatchUpgradeOperation methods now filter candidate devices by those valid IDs and _get_upgrader_class() catches ObjectDoesNotExist when a device has no DeviceConnection.

Sequence Diagram

sequenceDiagram
    participant User as User/Admin
    participant AdminView as AdminView (upgrade_selected)
    participant Model as BatchUpgradeOperation
    participant DB as Database
    participant Template as Confirmation Template

    User->>AdminView: POST mass-upgrade request
    AdminView->>DB: Query DeviceConnection(enabled=True, update_strategy__icontains="ssh")
    DB-->>AdminView: valid_device_ids
    AdminView->>DB: Query related DeviceFirmware & firmwareless Devices
    DB-->>AdminView: device_firmwares, devices
    AdminView->>AdminView: Exclude valid_device_ids -> credentialless lists
    AdminView->>AdminView: Build credentialless_devices (limit MAX_CREDENTIALLESS_DISPLAY) and credentialless_more
    AdminView->>Model: Request upgrader schema/class for candidates
    Model->>DB: Fetch DeviceConnection for device (via get_upgrader_class_for_device)
    alt Device has SSH credentials
        DB-->>Model: Connection found
        Model-->>AdminView: Upgrader class
    else Device lacks SSH credentials
        DB-->>Model: DoesNotExist
        Model->>Model: Catch exception and skip/select next candidate
        Model-->>AdminView: Upgrader class or None
    end
    AdminView->>Template: Render confirmation with credentialless_* context
    Template->>User: Show credentialless message + device links
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title includes required [fix] prefix, references issue #302, and accurately describes the main change: preventing 500 errors and adding UI warnings for devices lacking SSH credentials during mass upgrades.
Description check ✅ Passed The description comprehensively covers problem statement, detailed code changes across all modified files, test coverage, and references the linked issue #302. All required template sections are present and well-populated.
Linked Issues check ✅ Passed Code changes fully address issue #302 objectives: catches ObjectDoesNotExist exception in _get_upgrader_class() to prevent 500 errors, selects first credentialed device via ORM, and surfaces credentialless devices on confirmation page with admin links.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to fixing issue #302: exception handling, credentialless device detection and display, and related test coverage. No unrelated modifications detected.
Bug Fixes ✅ Passed PR fixes bug #302 by catching ObjectDoesNotExist exception in _get_upgrader_class() with comprehensive credential filtering and four regression tests.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@Adityashandilya555 Adityashandilya555 force-pushed the fix/302-mass-upgrade-credentialless-devices branch from ac8e7d7 to f6cee89 Compare March 24, 2026 15:45
Copy link
Copy Markdown

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
openwisp_firmware_upgrader/admin.py (1)

265-317: ⚠️ Potential issue | 🟠 Major

This warning is never enforced on the successful confirm POST.

These credentialless_* querysets are only used to decorate the intermediate page. The upgrade_all / upgrade_related branch above still returns from build.batch_upgrade(...) before this logic affects batch creation, so admins can confirm a mass upgrade that still contains credentialless devices. Since AbstractBatchUpgradeOperation.upgrade_related_devices() / upgrade_firmwareless_devices() still call full_clean() on every selected device, that batch can still fail as soon as it reaches one of them. Either block submission when credentialless devices remain, or exclude them consistently from the actual upgrade path.

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

In `@openwisp_firmware_upgrader/admin.py` around lines 265 - 317, The page only
warns about credentialless devices but does not prevent POST submission, so
ensure the same credentialless filtering is applied before creating the batch:
in the POST handling path that calls build.batch_upgrade (the upgrade_all /
upgrade_related branch), detect credentialless devices using the same logic
(DeviceConnection filter used to compute credentialless_fw and
credentialless_firmwareless) and either (A) abort the POST with an
error/HttpResponseBadRequest informing the admin when total_credentialless > 0,
or (B) remove those credentialless entries from the related_device_fw and
firmwareless_devices passed into build.batch_upgrade so
AbstractBatchUpgradeOperation.upgrade_related_devices() /
upgrade_firmwareless_devices() never reaches full_clean() for credentialless
devices; reference credentialless_fw, credentialless_firmwareless,
total_credentialless, build.batch_upgrade and the
AbstractBatchUpgradeOperation.upgrade_related_devices()/upgrade_firmwareless_devices()
methods when implementing the check/filter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_firmware_upgrader/base/models.py`:
- Around line 705-723: The code uses "related_device_fw = related_device_fw or
self.build._find_related_device_firmwares(...)" and similarly for
"firmwareless_devices", which treats empty QuerySets as falsey and loses the
caller's scoped filter; change these to only call the build-scoped finder when
the incoming variable is None (e.g., if related_device_fw is None:
related_device_fw =
self.build._find_related_device_firmwares(select_devices=True)), and do the same
for firmwareless_devices (use is None checks), keeping the subsequent filtering
and calls to get_upgrader_class_for_device(df.device) and
get_upgrader_class_for_device(device) unchanged.

In
`@openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html`:
- Around line 38-42: The template uses "{% blocktrans with
count=credentialless_more %}" which prevents plural-aware translations; change
it to use a count-aware blocktrans by using "{% blocktrans count
count=credentialless_more %}" (keeping the variable name credentialless_more)
and add a "{% plural %}" section so you provide both singular and plural strings
referencing the count variable; update the text inside the singular and plural
branches to mention the count variable (e.g., "... and {{ count }} more device
without credentials (not shown)." and the plural "... and {{ count }} more
devices without credentials (not shown).") and close with "{% endblocktrans %}"
so translators receive proper plural forms.

In `@openwisp_firmware_upgrader/tests/test_admin.py`:
- Around line 249-289: The test
test_upgrade_intermediate_page_credentialless_more_than_max only asserts banner
text but not the actual truncation logic; after posting to build_list_url check
the response.context keys used by the view/template (e.g.,
'credentialless_devices' and 'credentialless_more') so the overflow logic is
validated: assert len(response.context['credentialless_devices']) ==
MAX_CREDENTIALLESS_DISPLAY and assert response.context['credentialless_more'] ==
(total_credentialless_count - MAX_CREDENTIALLESS_DISPLAY) (where
total_credentialless_count is the number of devices created), and optionally
assert that a device past the display limit (e.g.,
devices[MAX_CREDENTIALLESS_DISPLAY]) is not included in
response.context['credentialless_devices'] while an earlier device is included;
reference the test name
test_upgrade_intermediate_page_credentialless_more_than_max, the constant
MAX_CREDENTIALLESS_DISPLAY, ACTION_CHECKBOX_NAME and build_list_url to find
where to add these assertions.

---

Outside diff comments:
In `@openwisp_firmware_upgrader/admin.py`:
- Around line 265-317: The page only warns about credentialless devices but does
not prevent POST submission, so ensure the same credentialless filtering is
applied before creating the batch: in the POST handling path that calls
build.batch_upgrade (the upgrade_all / upgrade_related branch), detect
credentialless devices using the same logic (DeviceConnection filter used to
compute credentialless_fw and credentialless_firmwareless) and either (A) abort
the POST with an error/HttpResponseBadRequest informing the admin when
total_credentialless > 0, or (B) remove those credentialless entries from the
related_device_fw and firmwareless_devices passed into build.batch_upgrade so
AbstractBatchUpgradeOperation.upgrade_related_devices() /
upgrade_firmwareless_devices() never reaches full_clean() for credentialless
devices; reference credentialless_fw, credentialless_firmwareless,
total_credentialless, build.batch_upgrade and the
AbstractBatchUpgradeOperation.upgrade_related_devices()/upgrade_firmwareless_devices()
methods when implementing the check/filter.
🪄 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: d18be7ef-1db5-49f9-9b1e-127a6cc6beae

📥 Commits

Reviewing files that changed from the base of the PR and between c142d19 and ac8e7d7.

📒 Files selected for processing (4)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/test_admin.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.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=4.2.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.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2026-02-24T00:04:16.310Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-options.css:11-13
Timestamp: 2026-02-24T00:04:16.310Z
Learning: In the openwisp-firmware-upgrader project, hiding empty submit rows with CSS (e.g., `#upgradeoperation_form .submit-row { display: none; }`) is preferred over removing them in templates for simplicity and speed.

Applied to files:

  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-03-07T01:16:24.447Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-27T19:09:01.705Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:09:01.705Z
Learning: In openwisp-firmware-upgrader tests, the BaseTestAdmin class should derive app_label from Build._meta.app_label and config_app_label from Device._meta.app_label instead of hard-coding them. This ensures tests work correctly when models are swapped.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-23T21:36:30.581Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:30.581Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, the sample app (tests/openwisp2/sample_firmware_upgrader) can have a different migration path from the main app. New migration files are generally not created for small changes in the sample app since it's only used for demonstration and testing purposes. The sample app's initial migration may include fields or choices that are added incrementally in the main app's migration history.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py

Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/tests/test_admin.py Outdated
Copy link
Copy Markdown

@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.

Caution

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

⚠️ Outside diff range comments (1)
openwisp_firmware_upgrader/admin.py (1)

265-319: ⚠️ Potential issue | 🟠 Major

The credential filter never reaches the confirmed upgrade path.

This block only builds template metadata. The follow-up POST still calls build.batch_upgrade() with just group / location, so the devices listed here remain part of the batch if the admin clicks through. Once upgrade_related_devices() or upgrade_firmwareless_devices() reaches one of them, DeviceFirmware.full_clean() raises because deviceconnection_set is empty. Exclude credentialless devices in the batch path as well, or block submission when no upgradeable devices remain.

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

In `@openwisp_firmware_upgrader/admin.py` around lines 265 - 319, The UI-only
credentialless filter currently only builds template metadata
(credentialless_devices) but does not remove those devices from the actual
batch, so when the POST path still calls
BatchUpgradeOperation(build=build).batch_upgrade(...) with the original
related_device_fw and firmwareless_devices the upgrade functions
(upgrade_related_devices / upgrade_firmwareless_devices) will hit
DeviceFirmware.full_clean() because DeviceConnection objects are missing; fix by
filtering out credentialless devices from the batch before calling batch_upgrade
(or abort the POST if no upgradeable devices remain): identify credentialless
IDs using DeviceConnection (same logic as used to compute credentialless_fw and
credentialless_firmwareless), remove those device IDs from related_device_fw and
firmwareless_devices passed into BatchUpgradeOperation.build.batch_upgrade (or
return an error/redirect when resulting queryset/count is zero) so only devices
with valid SSH credentials are processed.
♻️ Duplicate comments (2)
openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html (1)

38-42: ⚠️ Potential issue | 🟡 Minor

Make the overflow message plural-aware.

{% blocktrans with %} gives translators only one message id here, so credentialless_more cannot be localized with proper singular/plural forms. Use {% blocktrans count ... %} with a {% plural %} branch instead.

Suggested fix
-          {% blocktrans with count=credentialless_more %}
-            ... and {{ count }} more device(s) without credentials (not shown).
+          {% blocktrans count count=credentialless_more %}
+            ... and {{ count }} more device without credentials (not shown).
+          {% plural %}
+            ... and {{ count }} more devices without credentials (not shown).
           {% endblocktrans %}
In Django templates, should count-dependent translated strings use `{% blocktrans count ... %}` with a `{% plural %}` branch instead of `{% blocktrans with ... %}`?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html`
around lines 38 - 42, Replace the current `{% blocktrans with
count=credentialless_more %}` usage so the translation can handle
singular/plural forms: use `{% blocktrans count credentialless_more as count %}`
and add a `{% plural %}` branch that renders the plural message; ensure you
reference the same `credentialless_more` variable and keep existing surrounding
markup and the `if credentialless_more` check.
openwisp_firmware_upgrader/base/models.py (1)

705-719: ⚠️ Potential issue | 🟠 Major

Keep the caller’s scoped QuerySets when they’re empty.

dry_run() passes filtered QuerySets into _get_upgrader_class(). Using qs or ... treats an intentionally empty scope as falsey and falls back to build-wide devices, so the confirmation page can derive its upgrader/schema from outside the selected group or location.

Suggested fix
-        related_device_fw = (
-            related_device_fw
-            or self.build._find_related_device_firmwares(select_devices=True)
-        )
+        if related_device_fw is None:
+            related_device_fw = self.build._find_related_device_firmwares(
+                select_devices=True
+            )
@@
-        firmwareless_devices = (
-            firmwareless_devices or self.build._find_firmwareless_devices()
-        )
+        if firmwareless_devices is None:
+            firmwareless_devices = self.build._find_firmwareless_devices()
In Django, are empty QuerySet objects falsy in boolean contexts, and does `qs or fallback_qs` replace an intentionally empty filtered QuerySet with the fallback?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_firmware_upgrader/base/models.py` around lines 705 - 719, The code
uses "related_device_fw = related_device_fw or
self.build._find_related_device_firmwares(...)" and similarly for
"firmwareless_devices", which treats an intentionally empty QuerySet as falsey
and replaces it with the build-wide QuerySet; instead, preserve caller-scoped
empty QuerySets by only supplying the fallback when the parameter is None (not
falsy). Update _get_upgrader_class (and calls from dry_run) to set
related_device_fw and firmwareless_devices using an "is None" check (e.g., if
related_device_fw is None: related_device_fw =
self.build._find_related_device_firmwares(...)) so empty filtered QuerySets
remain unchanged, ensuring get_upgrader_class_for_device(df.device) and
downstream logic use the caller's scope.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@openwisp_firmware_upgrader/admin.py`:
- Around line 265-319: The UI-only credentialless filter currently only builds
template metadata (credentialless_devices) but does not remove those devices
from the actual batch, so when the POST path still calls
BatchUpgradeOperation(build=build).batch_upgrade(...) with the original
related_device_fw and firmwareless_devices the upgrade functions
(upgrade_related_devices / upgrade_firmwareless_devices) will hit
DeviceFirmware.full_clean() because DeviceConnection objects are missing; fix by
filtering out credentialless devices from the batch before calling batch_upgrade
(or abort the POST if no upgradeable devices remain): identify credentialless
IDs using DeviceConnection (same logic as used to compute credentialless_fw and
credentialless_firmwareless), remove those device IDs from related_device_fw and
firmwareless_devices passed into BatchUpgradeOperation.build.batch_upgrade (or
return an error/redirect when resulting queryset/count is zero) so only devices
with valid SSH credentials are processed.

---

Duplicate comments:
In `@openwisp_firmware_upgrader/base/models.py`:
- Around line 705-719: The code uses "related_device_fw = related_device_fw or
self.build._find_related_device_firmwares(...)" and similarly for
"firmwareless_devices", which treats an intentionally empty QuerySet as falsey
and replaces it with the build-wide QuerySet; instead, preserve caller-scoped
empty QuerySets by only supplying the fallback when the parameter is None (not
falsy). Update _get_upgrader_class (and calls from dry_run) to set
related_device_fw and firmwareless_devices using an "is None" check (e.g., if
related_device_fw is None: related_device_fw =
self.build._find_related_device_firmwares(...)) so empty filtered QuerySets
remain unchanged, ensuring get_upgrader_class_for_device(df.device) and
downstream logic use the caller's scope.

In
`@openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html`:
- Around line 38-42: Replace the current `{% blocktrans with
count=credentialless_more %}` usage so the translation can handle
singular/plural forms: use `{% blocktrans count credentialless_more as count %}`
and add a `{% plural %}` branch that renders the plural message; ensure you
reference the same `credentialless_more` variable and keep existing surrounding
markup and the `if credentialless_more` check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0343a51b-3143-4017-9123-379c4c4a9351

📥 Commits

Reviewing files that changed from the base of the PR and between ac8e7d7 and f6cee89.

📒 Files selected for processing (4)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/test_admin.py
📜 Review details
🧰 Additional context used
🧠 Learnings (11)
📚 Learning: 2026-02-24T00:04:16.310Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-options.css:11-13
Timestamp: 2026-02-24T00:04:16.310Z
Learning: In the openwisp-firmware-upgrader project, hiding empty submit rows with CSS (e.g., `#upgradeoperation_form .submit-row { display: none; }`) is preferred over removing them in templates for simplicity and speed.

Applied to files:

  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-03-07T01:16:24.447Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-27T19:09:01.705Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:09:01.705Z
Learning: In openwisp-firmware-upgrader tests, the BaseTestAdmin class should derive app_label from Build._meta.app_label and config_app_label from Device._meta.app_label instead of hard-coding them. This ensures tests work correctly when models are swapped.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-23T21:36:30.581Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:30.581Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, the sample app (tests/openwisp2/sample_firmware_upgrader) can have a different migration path from the main app. New migration files are generally not created for small changes in the sample app since it's only used for demonstration and testing purposes. The sample app's initial migration may include fields or choices that are added incrementally in the main app's migration history.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-21T20:21:04.228Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:04.228Z
Learning: In openwisp-firmware-upgrader project, the team prefers not to add explicit `related_name` to ForeignKey fields in migrations when the default reverse accessor (`<model>_set`) is acceptable. They are comfortable with Django's default naming convention.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-26T17:22:26.110Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-26T17:22:26.110Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, avoid making suggestions about UI/frontend code (JavaScript DOM traversal, CSS selectors, jQuery operations) unless there is a clear, objective bug (e.g., syntax error, undefined variable). UI code structure and DOM traversal paths should be verified through manual testing by the maintainers, as suggestions without access to the actual rendered DOM can break functionality.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-25T00:17:23.479Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py

@Adityashandilya555 Adityashandilya555 force-pushed the fix/302-mass-upgrade-credentialless-devices branch from f6cee89 to 47797ec Compare March 24, 2026 16:33
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
openwisp_firmware_upgrader/tests/test_admin.py (1)

279-289: ⚠️ Potential issue | 🟡 Minor

Assert the truncation state, not just the overflow sentence.

This still passes if credentialless_more is off by one or if the template renders more than MAX_CREDENTIALLESS_DISPLAY devices. Assert the context values used by the view instead of only checking the raw device(s) text.

Tighten the test
         self.assertEqual(r.status_code, 200)
         self.assertContains(r, "no SSH credentials assigned")
-        self.assertContains(r, "more device(s) without credentials")
+        self.assertEqual(r.context["credentialless_more"], 2)
+        self.assertEqual(
+            len(r.context["credentialless_devices"]), MAX_CREDENTIALLESS_DISPLAY
+        )
+        self.assertContains(r, "... and 2 more")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_firmware_upgrader/tests/test_admin.py` around lines 279 - 289, The
test currently only checks rendered text; instead assert the view context to
ensure truncation is correct: after posting to self.build_list_url with
ACTION_CHECKBOX_NAME, assert r.context['credentialless_more'] is True/False as
appropriate and assert len(r.context['credentialless']) ==
MAX_CREDENTIALLESS_DISPLAY (or equals the expected number) so the template
truncation is validated; reference the test method that posts to
self.build_list_url and the context keys 'credentialless' and
'credentialless_more' to locate where to add these assertions.
openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html (1)

38-42: ⚠️ Potential issue | 🟡 Minor

Use a count-aware blocktrans for the overflow notice.

blocktrans with gives translators only one source string, so singular/plural forms of credentialless_more cannot be localized correctly here.

Suggested fix
-          {% blocktrans with count=credentialless_more %}
-            ... and {{ count }} more device(s) without credentials (not shown).
+          {% blocktrans count count=credentialless_more %}
+            ... and {{ count }} more device without credentials (not shown).
+          {% plural %}
+            ... and {{ count }} more devices without credentials (not shown).
           {% endblocktrans %}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html`
around lines 38 - 42, Replace the current non-count-aware blocktrans using "with
count=credentialless_more" with a count-aware pluralization blocktrans by using
"blocktrans count credentialless_more as count" and adding a {% plural %}
section; reference the credentialless_more variable and the existing blocktrans
block so translators can provide singular and plural forms and render {{ count
}} correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_firmware_upgrader/base/models.py`:
- Around line 700-722: The code currently filters devices with credentials only
for schema selection (using valid_device_ids from DeviceConnection) but later
execution in upgrade_related_devices() and upgrade_firmwareless_devices() still
iterates over unfiltered device querysets causing AbstractDeviceFirmware.clean()
to raise for credentialless devices; update those execution querysets to reuse
the same valid_device_ids filter (or add a guard right before calling
full_clean()/save to skip devices where device.deviceconnection_set.count() < 1)
so only devices with DeviceConnection entries are processed—look for references
to upgrade_related_devices, upgrade_firmwareless_devices,
AbstractDeviceFirmware.clean, and valid_device_ids to apply the fix.

---

Duplicate comments:
In
`@openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html`:
- Around line 38-42: Replace the current non-count-aware blocktrans using "with
count=credentialless_more" with a count-aware pluralization blocktrans by using
"blocktrans count credentialless_more as count" and adding a {% plural %}
section; reference the credentialless_more variable and the existing blocktrans
block so translators can provide singular and plural forms and render {{ count
}} correctly.

In `@openwisp_firmware_upgrader/tests/test_admin.py`:
- Around line 279-289: The test currently only checks rendered text; instead
assert the view context to ensure truncation is correct: after posting to
self.build_list_url with ACTION_CHECKBOX_NAME, assert
r.context['credentialless_more'] is True/False as appropriate and assert
len(r.context['credentialless']) == MAX_CREDENTIALLESS_DISPLAY (or equals the
expected number) so the template truncation is validated; reference the test
method that posts to self.build_list_url and the context keys 'credentialless'
and 'credentialless_more' to locate where to add these assertions.
🪄 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: 6a783cee-8f4c-488f-81c9-a4d373e10674

📥 Commits

Reviewing files that changed from the base of the PR and between f6cee89 and 47797ec.

📒 Files selected for processing (4)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/test_admin.py
📜 Review details
🧰 Additional context used
🧠 Learnings (12)
📚 Learning: 2026-02-24T00:04:16.310Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-options.css:11-13
Timestamp: 2026-02-24T00:04:16.310Z
Learning: In the openwisp-firmware-upgrader project, hiding empty submit rows with CSS (e.g., `#upgradeoperation_form .submit-row { display: none; }`) is preferred over removing them in templates for simplicity and speed.

Applied to files:

  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
📚 Learning: 2026-02-26T17:22:26.110Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-26T17:22:26.110Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, avoid making suggestions about UI/frontend code (JavaScript DOM traversal, CSS selectors, jQuery operations) unless there is a clear, objective bug (e.g., syntax error, undefined variable). UI code structure and DOM traversal paths should be verified through manual testing by the maintainers, as suggestions without access to the actual rendered DOM can break functionality.

Applied to files:

  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-23T21:36:30.581Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:30.581Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, the sample app (tests/openwisp2/sample_firmware_upgrader) can have a different migration path from the main app. New migration files are generally not created for small changes in the sample app since it's only used for demonstration and testing purposes. The sample app's initial migration may include fields or choices that are added incrementally in the main app's migration history.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-03-07T01:16:24.447Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-27T19:09:01.705Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:09:01.705Z
Learning: In openwisp-firmware-upgrader tests, the BaseTestAdmin class should derive app_label from Build._meta.app_label and config_app_label from Device._meta.app_label instead of hard-coding them. This ensures tests work correctly when models are swapped.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-25T00:17:23.479Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-25T18:15:31.491Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-25T18:15:31.491Z
Learning: For large PRs (5000+ LOC) in the openwisp/openwisp-firmware-upgrader repository, be ruthlessly practical: only flag bugs, security issues, data loss risks, or broken functionality. Never flag theoretical concerns, style preferences, hypothetical refactorings, or issues that are functionally correct. The team needs to ship, not debate imaginary problems.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-21T20:21:04.228Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:04.228Z
Learning: In openwisp-firmware-upgrader project, the team prefers not to add explicit `related_name` to ForeignKey fields in migrations when the default reverse accessor (`<model>_set`) is acceptable. They are comfortable with Django's default naming convention.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py

Comment thread openwisp_firmware_upgrader/base/models.py
@Adityashandilya555 Adityashandilya555 force-pushed the fix/302-mass-upgrade-credentialless-devices branch from 47797ec to 0d96cce Compare March 24, 2026 17:09
Copy link
Copy Markdown

@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 (2)
openwisp_firmware_upgrader/tests/test_admin.py (1)

249-289: 🧹 Nitpick | 🔵 Trivial

Consider adding context assertions for more robust overflow testing.

The test verifies the UI text is present, but asserting the actual context values would catch edge cases like off-by-one errors in the overflow calculation.

Suggested improvement
         self.assertEqual(r.status_code, 200)
         self.assertContains(r, "no SSH credentials assigned")
         self.assertContains(r, "more device(s) without credentials")
+        # Verify the overflow logic is correct
+        self.assertEqual(r.context["credentialless_more"], 2)
+        self.assertEqual(
+            len(r.context["credentialless_devices"]), MAX_CREDENTIALLESS_DISPLAY
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_firmware_upgrader/tests/test_admin.py` around lines 249 - 289, After
the POST (r = self.client.post(...)) add assertions that validate the view
context payload for the credentialless overflow: check that r.context contains
the expected keys (e.g. 'credentialless_devices' and a total/overflow count key
such as 'credentialless_total' or 'credentialless_overflow_count'), assert
len(r.context['credentialless_devices']) == MAX_CREDENTIALLESS_DISPLAY and
assert the total/overflow count equals MAX_CREDENTIALLESS_DISPLAY + 2 (or that
overflow flag is True) so the test fails on off-by-one or incorrect overflow
calculation; reference MAX_CREDENTIALLESS_DISPLAY, build_list_url and
ACTION_CHECKBOX_NAME to locate the POST/test block.
openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html (1)

38-43: ⚠️ Potential issue | 🟡 Minor

Overflow message still uses blocktrans with instead of blocktrans count.

The main credentialless message at lines 27-31 correctly uses blocktrans count with {% plural %}, but the overflow message at lines 40-42 still uses blocktrans with, which doesn't allow proper pluralization for translators.

Suggested fix for proper pluralization
       {% if credentialless_more %}
         <p>
-          {% blocktrans with count=credentialless_more %}
-            ... and {{ count }} more device(s) without credentials (not shown).
+          {% blocktrans count count=credentialless_more %}
+            ... and {{ count }} more device without credentials (not shown).
+          {% plural %}
+            ... and {{ count }} more devices without credentials (not shown).
           {% endblocktrans %}
         </p>
       {% endif %}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html`
around lines 38 - 43, The overflow message uses "{% blocktrans with
count=credentialless_more %}" which prevents proper pluralization; replace it
with a plural-aware blocktrans (e.g. "{% blocktrans count
count=credentialless_more %}" and include a "{% plural %}" section) and
reference the variable {{ count }} inside both singular and plural forms so
translators can provide correct plural translations for credentialless_more.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html`:
- Around line 38-43: The overflow message uses "{% blocktrans with
count=credentialless_more %}" which prevents proper pluralization; replace it
with a plural-aware blocktrans (e.g. "{% blocktrans count
count=credentialless_more %}" and include a "{% plural %}" section) and
reference the variable {{ count }} inside both singular and plural forms so
translators can provide correct plural translations for credentialless_more.

In `@openwisp_firmware_upgrader/tests/test_admin.py`:
- Around line 249-289: After the POST (r = self.client.post(...)) add assertions
that validate the view context payload for the credentialless overflow: check
that r.context contains the expected keys (e.g. 'credentialless_devices' and a
total/overflow count key such as 'credentialless_total' or
'credentialless_overflow_count'), assert
len(r.context['credentialless_devices']) == MAX_CREDENTIALLESS_DISPLAY and
assert the total/overflow count equals MAX_CREDENTIALLESS_DISPLAY + 2 (or that
overflow flag is True) so the test fails on off-by-one or incorrect overflow
calculation; reference MAX_CREDENTIALLESS_DISPLAY, build_list_url and
ACTION_CHECKBOX_NAME to locate the POST/test block.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7dc028b3-6b5a-4632-a7d0-ebf1f0de65e9

📥 Commits

Reviewing files that changed from the base of the PR and between 47797ec and 0d96cce.

📒 Files selected for processing (4)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/test_admin.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.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.13 | 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.10 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
🧰 Additional context used
🧠 Learnings (13)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.
📚 Learning: 2026-02-24T00:04:16.310Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-options.css:11-13
Timestamp: 2026-02-24T00:04:16.310Z
Learning: In the openwisp-firmware-upgrader project, hiding empty submit rows with CSS (e.g., `#upgradeoperation_form .submit-row { display: none; }`) is preferred over removing them in templates for simplicity and speed.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T00:17:23.479Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-23T21:36:30.581Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:30.581Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, the sample app (tests/openwisp2/sample_firmware_upgrader) can have a different migration path from the main app. New migration files are generally not created for small changes in the sample app since it's only used for demonstration and testing purposes. The sample app's initial migration may include fields or choices that are added incrementally in the main app's migration history.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-26T17:22:26.110Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-26T17:22:26.110Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, avoid making suggestions about UI/frontend code (JavaScript DOM traversal, CSS selectors, jQuery operations) unless there is a clear, objective bug (e.g., syntax error, undefined variable). UI code structure and DOM traversal paths should be verified through manual testing by the maintainers, as suggestions without access to the actual rendered DOM can break functionality.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-03-07T01:16:24.447Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-27T19:09:01.705Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:09:01.705Z
Learning: In openwisp-firmware-upgrader tests, the BaseTestAdmin class should derive app_label from Build._meta.app_label and config_app_label from Device._meta.app_label instead of hard-coding them. This ensures tests work correctly when models are swapped.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-25T18:15:31.491Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-25T18:15:31.491Z
Learning: For large PRs (5000+ LOC) in the openwisp/openwisp-firmware-upgrader repository, be ruthlessly practical: only flag bugs, security issues, data loss risks, or broken functionality. Never flag theoretical concerns, style preferences, hypothetical refactorings, or issues that are functionally correct. The team needs to ship, not debate imaginary problems.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-21T20:21:04.228Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:04.228Z
Learning: In openwisp-firmware-upgrader project, the team prefers not to add explicit `related_name` to ForeignKey fields in migrations when the default reverse accessor (`<model>_set`) is acceptable. They are comfortable with Django's default naming convention.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
🔇 Additional comments (7)
openwisp_firmware_upgrader/base/models.py (2)

615-621: LGTM! Credential filtering now applied in execution paths.

The upgrade_related_devices() and upgrade_firmwareless_devices() methods now correctly filter devices to only those with enabled SSH credentials before calling full_clean(), preventing the ValidationError that would occur when devices lack a DeviceConnection.

Also applies to: 637-646


702-730: LGTM! Robust fallback logic with proper exception handling and credential filtering.

The method now:

  1. Catches ObjectDoesNotExist for the initial upgrade_operations lookup
  2. Uses is None checks instead of or to preserve scoped querysets (addresses past review)
  3. Filters fallback candidates by valid_device_ids to ensure only devices with enabled SSH credentials are considered

This prevents the 500 error when devices lack credentials.

openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html (1)

24-36: LGTM! Proper error notification for credentialless devices.

The template correctly:

  1. Uses the errornote class for consistent Django admin styling
  2. Provides proper plural-aware messaging via blocktrans count
  3. Lists affected devices with admin change links for easy navigation
openwisp_firmware_upgrader/admin.py (2)

265-303: LGTM! Well-structured credentialless device detection.

The implementation:

  1. Uses the same credential filter criteria (update_strategy__icontains="ssh", enabled=True) as base/models.py, ensuring consistency
  2. Correctly excludes valid devices from both related_device_fw and firmwareless_devices
  3. Prioritizes firmware-related devices in the display list before firmwareless ones
  4. Caps the display list at MAX_CREDENTIALLESS_DISPLAY with proper overflow calculation

317-319: LGTM!

Context variables correctly passed to the template for rendering the credentialless warning.

openwisp_firmware_upgrader/tests/test_admin.py (2)

190-247: LGTM! Comprehensive test coverage for credentialless scenarios.

The tests properly cover:

  1. Related devices without credentials
  2. Firmwareless devices without credentials
  3. All devices credentialless (verifies no 500 error)
  4. Proper assertions for device change URLs and names

161-161: Query count increases are expected.

The additional queries (15→21 and 14→18) are reasonable given the new credentialless detection logic that requires:

  • Filtering DeviceConnection for valid SSH credentials
  • Computing credentialless subsets via exclusion
  • Fetching device details for the warning display

Also applies to: 175-175

@Adityashandilya555 Adityashandilya555 force-pushed the fix/302-mass-upgrade-credentialless-devices branch from 0d96cce to 6ca4d31 Compare March 24, 2026 17:28
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In
`@openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html`:
- Line 30: In the plural branch of the template
admin/upgrade_selected_confirmation.html (the line containing "The following {{
count }} device(s) cannot be upgraded because they have no SSH credentials
assigned. Please assign credentials to proceed with the upgrade."), remove the
redundant "(s)" and change "device(s)" to the plural form "devices" so the
message reads "The following {{ count }} devices cannot be upgraded..."; update
only that plural branch string in the template.
🪄 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: 378e7c76-da8d-4124-9718-7827c45f5ca3

📥 Commits

Reviewing files that changed from the base of the PR and between 0d96cce and 6ca4d31.

📒 Files selected for processing (4)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/test_admin.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.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
🧰 Additional context used
🧠 Learnings (13)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.
📚 Learning: 2026-02-24T00:04:16.310Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-options.css:11-13
Timestamp: 2026-02-24T00:04:16.310Z
Learning: In the openwisp-firmware-upgrader project, hiding empty submit rows with CSS (e.g., `#upgradeoperation_form .submit-row { display: none; }`) is preferred over removing them in templates for simplicity and speed.

Applied to files:

  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-26T17:22:26.110Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-26T17:22:26.110Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, avoid making suggestions about UI/frontend code (JavaScript DOM traversal, CSS selectors, jQuery operations) unless there is a clear, objective bug (e.g., syntax error, undefined variable). UI code structure and DOM traversal paths should be verified through manual testing by the maintainers, as suggestions without access to the actual rendered DOM can break functionality.

Applied to files:

  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-23T21:36:30.581Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:30.581Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, the sample app (tests/openwisp2/sample_firmware_upgrader) can have a different migration path from the main app. New migration files are generally not created for small changes in the sample app since it's only used for demonstration and testing purposes. The sample app's initial migration may include fields or choices that are added incrementally in the main app's migration history.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-25T00:17:23.479Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-03-07T01:16:24.447Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py
  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-21T20:21:04.228Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:04.228Z
Learning: In openwisp-firmware-upgrader project, the team prefers not to add explicit `related_name` to ForeignKey fields in migrations when the default reverse accessor (`<model>_set`) is acceptable. They are comfortable with Django's default naming convention.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-27T19:09:01.705Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:09:01.705Z
Learning: In openwisp-firmware-upgrader tests, the BaseTestAdmin class should derive app_label from Build._meta.app_label and config_app_label from Device._meta.app_label instead of hard-coding them. This ensures tests work correctly when models are swapped.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-25T18:15:31.491Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-25T18:15:31.491Z
Learning: For large PRs (5000+ LOC) in the openwisp/openwisp-firmware-upgrader repository, be ruthlessly practical: only flag bugs, security issues, data loss risks, or broken functionality. Never flag theoretical concerns, style preferences, hypothetical refactorings, or issues that are functionally correct. The team needs to ship, not debate imaginary problems.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
🔇 Additional comments (9)
openwisp_firmware_upgrader/base/models.py (2)

615-621: LGTM on credential filtering in upgrade methods.

The filtering logic correctly restricts upgrades to devices with valid SSH-enabled DeviceConnection records. Using swapper.load_model is appropriate for the swappable model, and the filtering is consistently applied in both upgrade_related_devices and upgrade_firmwareless_devices.

Also applies to: 637-646


704-730: LGTM on the _get_upgrader_class improvements.

The changes correctly:

  1. Catch ObjectDoesNotExist when accessing self.upgrade_operations[0].device (addressing the 500 error)
  2. Use is None checks instead of or operator to preserve scoped querysets from the admin dry-run
  3. Apply consistent SSH credential filtering via valid_device_ids
openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html (1)

24-48: LGTM on the credentialless warning block.

The template correctly implements the warning UI with:

  • Proper blocktrans count for i18n pluralization (addressing past feedback)
  • Clickable device links for easy navigation
  • Overflow message with singular/plural forms
openwisp_firmware_upgrader/admin.py (3)

43-43: LGTM on the display limit constant.

Having this as a named constant makes it easy to adjust and test against.


265-303: LGTM on credentialless device identification logic.

The implementation correctly:

  1. Uses consistent filtering criteria (update_strategy__icontains="ssh" and enabled=True)
  2. Handles both related device firmwares and firmwareless devices
  3. Respects the display limit with proper overflow calculation
  4. Builds admin URLs for easy device access

317-319: LGTM on context variables.

The context correctly passes credentialless_devices, credentialless_count, and credentialless_more to the template.

openwisp_firmware_upgrader/tests/test_admin.py (3)

161-161: LGTM on query count adjustments.

The increased query counts (15→21 and 14→18) are expected due to the additional DeviceConnection queries for credential filtering.

Also applies to: 175-175


190-247: LGTM on the credentialless test cases.

Good coverage of:

  • Related devices without credentials
  • Firmwareless devices without credentials
  • All devices without credentials (critical for preventing the 500 error from #302)

249-298: LGTM on the overflow test case.

The test correctly validates the truncation behavior by:

  1. Creating MAX_CREDENTIALLESS_DISPLAY + 2 devices without credentials
  2. Asserting credentialless_devices list length equals MAX_CREDENTIALLESS_DISPLAY
  3. Asserting credentialless_more equals the overflow count (2)

This addresses the past review feedback about asserting actual truncation values, not just banner text.

Comment thread openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html Outdated
openwisp#302

- _get_upgrader_class() now uses an ORM subquery to find the first device
  with a valid SSH credential instead of blindly taking the first device,
  preventing DoesNotExist crashes when all selected devices lack credentials
- upgrade_selected() collects devices without SSH credentials after dry_run()
  and passes them to the confirmation template with admin URLs
- Confirmation page shows credentialless devices as clickable links inside
  an errornote warning block; limits display to MAX_CREDENTIALLESS_DISPLAY (5)
  and appends an "and N more" message when the list is truncated
- Added 4 tests covering: credentialless related device, credentialless
  firmwareless device, all-credentialless scenario (no 500), and overflow
  beyond the display limit

Fixes openwisp#302
@Adityashandilya555 Adityashandilya555 force-pushed the fix/302-mass-upgrade-credentialless-devices branch from 6ca4d31 to fbb21af Compare March 24, 2026 17:37
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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: 3

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

Inline comments:
In `@openwisp_firmware_upgrader/base/models.py`:
- Around line 615-621: The pre-check path currently uses the unfiltered
dry_run() results which can include credentialless devices; update the
validation in AbstractBuild.batch_upgrade() to apply the same credential filter
using DeviceConnection and valid_device_ids so fully credentialless submissions
are rejected before creating a batch. Specifically, compute valid_device_ids via
swapper.load_model("connection", "DeviceConnection")
filter(update_strategy__icontains="ssh", enabled=True).values("device_id") and
then use
dry_run_result["device_firmwares"].filter(device_id__in=valid_device_ids).exists()
and (if firmwareless)
dry_run_result["devices"].filter(pk__in=valid_device_ids).exists() to decide
whether to raise the existing ValidationError instead of proceeding to create
the batch.
- Around line 713-726: The fallback upgrader lookup ignores the batch's scope so
validation can pick a device outside the batch's group/location; update the
logic in the infer-upgrader section (around related_device_fw,
self.build._find_related_device_firmwares, firmwareless_devices and
self.build._find_firmwareless_devices) to pass the batch's group and location
when resolving devices (e.g., include self.group and self.location as
filters/arguments to _find_related_device_firmwares and
_find_firmwareless_devices) and then use the resolved df.device with
get_upgrader_class_for_device as before so the inferred upgrader respects the
batch scope.

In
`@openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html`:
- Around line 27-30: The warning message in the upgrade confirmation (the block
using blocktrans with count=credentialless_count) incorrectly implies the entire
batch is blocked; update the copy in that block (and any related text
referencing credentialless_count or credentialless devices) to clearly state
that devices without SSH credentials will be skipped while the rest of the batch
will proceed (e.g., "The following {{ count }} device(s) have no SSH credentials
and will be skipped; devices with credentials will continue to be upgraded."),
and ensure the message references credentialless_count and/or the list of
skipped devices so admins understand only those devices are excluded.
🪄 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: 9d89797f-08e3-4dbd-87d4-20c3f61386af

📥 Commits

Reviewing files that changed from the base of the PR and between 0d96cce and fbb21af.

📒 Files selected for processing (4)
  • openwisp_firmware_upgrader/admin.py
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/tests/test_admin.py
📜 Review details
🧰 Additional context used
🧠 Learnings (13)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.
📚 Learning: 2026-02-24T00:04:16.310Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/css/upgrade-options.css:11-13
Timestamp: 2026-02-24T00:04:16.310Z
Learning: In the openwisp-firmware-upgrader project, hiding empty submit rows with CSS (e.g., `#upgradeoperation_form .submit-row { display: none; }`) is preferred over removing them in templates for simplicity and speed.

Applied to files:

  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-26T17:22:26.110Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-26T17:22:26.110Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, avoid making suggestions about UI/frontend code (JavaScript DOM traversal, CSS selectors, jQuery operations) unless there is a clear, objective bug (e.g., syntax error, undefined variable). UI code structure and DOM traversal paths should be verified through manual testing by the maintainers, as suggestions without access to the actual rendered DOM can break functionality.

Applied to files:

  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T00:17:23.479Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/static/firmware-upgrader/js/upgrade-progress.js:458-463
Timestamp: 2026-02-25T00:17:23.479Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, focus review comments on real, actionable issues rather than theoretical edge cases, trivialities, or potential future problems. Avoid flagging issues that are functionally correct but could theoretically become problems only if the code were significantly refactored or used in different contexts.

Applied to files:

  • openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html
  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-23T21:44:41.470Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/websockets.py:233-246
Timestamp: 2026-02-23T21:44:41.470Z
Learning: In openwisp-firmware-upgrader, the `upgrade_operations` attribute on `BatchUpgradeOperation` is a cached property that returns a queryset, so it should be accessed as `batch_operation.upgrade_operations.all()` rather than being called as a method.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-21T20:21:04.228Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: openwisp_firmware_upgrader/migrations/0015_add_group_to_batchupgradeoperation.py:16-26
Timestamp: 2026-02-21T20:21:04.228Z
Learning: In openwisp-firmware-upgrader project, the team prefers not to add explicit `related_name` to ForeignKey fields in migrations when the default reverse accessor (`<model>_set`) is acceptable. They are comfortable with Django's default naming convention.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-27T19:09:01.705Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:09:01.705Z
Learning: In openwisp-firmware-upgrader tests, the BaseTestAdmin class should derive app_label from Build._meta.app_label and config_app_label from Device._meta.app_label instead of hard-coding them. This ensures tests work correctly when models are swapped.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-02-23T21:36:30.581Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: tests/openwisp2/sample_firmware_upgrader/migrations/0001_initial.py:61-67
Timestamp: 2026-02-23T21:36:30.581Z
Learning: In the openwisp/openwisp-firmware-upgrader repository, the sample app (tests/openwisp2/sample_firmware_upgrader) can have a different migration path from the main app. New migration files are generally not created for small changes in the sample app since it's only used for demonstration and testing purposes. The sample app's initial migration may include fields or choices that are added incrementally in the main app's migration history.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T11:20:13.903Z
Learnt from: pandafy
Repo: openwisp/openwisp-firmware-upgrader PR: 377
File: runtests.py:27-38
Timestamp: 2026-02-25T11:20:13.903Z
Learning: In openwisp-firmware-upgrader, runtests.py now uses `execute_from_command_line()` to run Django tests in-process rather than via subprocess, so the `patch = subprocess` coverage directive is not needed in pyproject.toml.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-03-07T01:16:24.447Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:24.447Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. Model-level has_view_permission(request) is sufficient for batch breadcrumb visibility in UpgradeOperationAdmin.change_view; object-level permission checks are not needed.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
  • openwisp_firmware_upgrader/tests/test_admin.py
📚 Learning: 2026-02-25T18:15:31.491Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 0
File: :0-0
Timestamp: 2026-02-25T18:15:31.491Z
Learning: For large PRs (5000+ LOC) in the openwisp/openwisp-firmware-upgrader repository, be ruthlessly practical: only flag bugs, security issues, data loss risks, or broken functionality. Never flag theoretical concerns, style preferences, hypothetical refactorings, or issues that are functionally correct. The team needs to ship, not debate imaginary problems.

Applied to files:

  • openwisp_firmware_upgrader/base/models.py
📚 Learning: 2026-03-07T01:16:15.471Z
Learnt from: atif09
Repo: openwisp/openwisp-firmware-upgrader PR: 387
File: openwisp_firmware_upgrader/admin.py:420-423
Timestamp: 2026-03-07T01:16:15.471Z
Learning: In openwisp-firmware-upgrader, UpgradeOperation and BatchUpgradeOperation always share the same organization (BatchUpgradeOperation.build.category.organization). A user who can view an UpgradeOperation can always view its related BatchUpgradeOperation. For the UpgradeOperationAdmin.change_view, model-level has_view_permission(request) suffices to show breadcrumbs for batch, and object-level permission checks are not required.

Applied to files:

  • openwisp_firmware_upgrader/admin.py
📚 Learning: 2026-02-27T19:08:56.218Z
Learnt from: nemesifier
Repo: openwisp/openwisp-firmware-upgrader PR: 383
File: openwisp_firmware_upgrader/tests/test_admin.py:50-50
Timestamp: 2026-02-27T19:08:56.218Z
Learning: In tests for openwisp-firmware-upgrader, derive app_label values from model meta instead of hard-coding. Specifically, let BaseTestAdmin compute app_label from Build._meta.app_label and config_app_label from Device._meta.app_label to ensure tests remain correct if models are swapped. This pattern should apply to all test files under openwisp_firmware_upgrader/tests.

Applied to files:

  • openwisp_firmware_upgrader/tests/test_admin.py

Comment thread openwisp_firmware_upgrader/base/models.py
Comment thread openwisp_firmware_upgrader/base/models.py Outdated
Comment thread openwisp_firmware_upgrader/templates/admin/upgrade_selected_confirmation.html Outdated
- batch_upgrade: filter credentialless devices before 'no devices' check
  so all-credentialless submissions raise ValidationError instead of
  creating a no-op batch
- _get_upgrader_class: pass group/location when building fallback
  related_device_fw/firmwareless_devices queries so scoped batches
  validate against devices within the selected scope
- Template: clarify that credentialless devices are skipped while the
  upgrade proceeds for the remaining devices

Related to openwisp#302
@Adityashandilya555 Adityashandilya555 force-pushed the fix/302-mass-upgrade-credentialless-devices branch from 941ec72 to 7212812 Compare March 25, 2026 08:52
@SAMurai-16
Copy link
Copy Markdown

SAMurai-16 commented Mar 29, 2026

Hi @Adityashandilya555 i was going through your approach and i had a question
At what part is the credentialless_count being calculated in your approach?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Mass upgrade results in 500 internal server error if there's any device with no credentials assigned

2 participants